@borgresearch/odin-mcp 0.2.0 → 0.3.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/README.md +91 -16
- package/dist/chunk-4LCKW236.js +205 -0
- package/dist/chunk-4LCKW236.js.map +1 -0
- package/dist/chunk-D5FYYTYV.js +1211 -0
- package/dist/chunk-D5FYYTYV.js.map +1 -0
- package/dist/chunk-ODZB3YP2.js +113 -0
- package/dist/chunk-ODZB3YP2.js.map +1 -0
- package/dist/chunk-XHJYVUHG.js +195 -0
- package/dist/chunk-XHJYVUHG.js.map +1 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +12 -0
- package/dist/client.js.map +1 -0
- package/dist/guidance.d.ts +53 -0
- package/dist/guidance.js +18 -0
- package/dist/guidance.js.map +1 -0
- package/dist/http.d.ts +8 -0
- package/dist/http.js +34 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +15 -782
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +11 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +29 -0
- package/dist/tools.js +9 -0
- package/dist/tools.js.map +1 -0
- package/package.json +26 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/finding-actions.ts","../src/tools/finding-reference.ts","../src/tools/finding-tool.ts","../src/tools/finding-attachments.ts","../src/tools/finding-collaboration.ts","../src/tools/finding-pull-requests.ts","../src/tools/findings-overview.ts","../src/tools/list-findings.ts","../src/formatters/markdown.ts","../src/tools/additional-tools.ts","../src/tools/get-asset.ts","../src/tools/get-finding.ts","../src/tools/get-finding-retests.ts","../src/tools/get-pentest.ts","../src/tools/link-finding-pull-request.ts","../src/tools/list-assets.ts","../src/tools/list-pentests.ts","../src/tools/update-finding-status.ts","../src/tools.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { OdinApiError, type OdinClient } from \"../client.js\";\nimport type {\n FindingSubscriptionResponse,\n RetestRequestResponse,\n} from \"../types.js\";\nimport {\n findingReferencePath,\n findingReferenceValue,\n} from \"./finding-reference.js\";\nimport { findingToolInput, writeAnnotations } from \"./finding-tool.js\";\n\nexport const followFindingInput = findingToolInput({\n follow: z\n .boolean()\n .describe(\"True to follow the finding, false to stop following it.\"),\n});\n\nexport const followFindingTool = {\n name: \"follow_finding\",\n title: \"Follow or unfollow a finding\",\n description:\n \"Follow a finding to get its notifications, or stop following it. Requires write permission and acts as the API key creator. The assignee always follows their finding and can't unfollow it.\",\n annotations: { ...writeAnnotations, idempotentHint: true },\n inputSchema: followFindingInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof followFindingInput>) => {\n const data = await client.put<FindingSubscriptionResponse>(\n findingReferencePath(params, \"/subscription\"),\n { subscribed: params.follow },\n );\n const label = findingReferenceValue(params);\n return {\n markdown: data.subscribed\n ? `Following ${label}.`\n : `Stopped following ${label}.`,\n structured: data,\n };\n },\n};\n\nexport const requestFindingRetestInput = findingToolInput({});\n\nexport const requestFindingRetestTool = {\n name: \"request_finding_retest\",\n title: \"Request a finding retest\",\n description:\n \"Queue a manual retest that checks whether a finding is still exploitable. Spends credits, so confirm with the user first. Works for findings in REPORTED, ACKNOWLEDGED, MITIGATING or OPEN_FOR_RETEST status with no retest already queued or running. Requires write permission and acts as the API key creator. Read the result later with get_finding_retests.\",\n annotations: writeAnnotations,\n inputSchema: requestFindingRetestInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof requestFindingRetestInput>) => {\n try {\n const data = await client.post<RetestRequestResponse>(\n findingReferencePath(params, \"/retest\"),\n {},\n );\n return {\n markdown: `Queued retest ${data.retestId} for ${findingReferenceValue(params)}.`,\n structured: data,\n };\n } catch (error) {\n if (error instanceof OdinApiError && isRetestUnavailable(error.body)) {\n const reason = \"Retests aren't available for this organisation.\";\n return {\n markdown: reason,\n structured: { state: \"unavailable\", reason },\n };\n }\n throw error;\n }\n },\n};\n\nfunction isRetestUnavailable(body: unknown): boolean {\n return (\n typeof body === \"object\" &&\n body !== null &&\n (body as { code?: unknown }).code === \"RETEST_UNAVAILABLE\"\n );\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\n\nexport const findingReferenceFields = {\n identifier: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n 'Human-readable finding identifier such as \"ODI-12\". Preferred input.',\n ),\n findingId: z\n .string()\n .trim()\n .min(1)\n .optional()\n .describe(\n \"Raw internal finding ID, useful when chaining from another tool.\",\n ),\n};\n\nexport const findingReferenceInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport type FindingReferenceInput = z.infer<typeof findingReferenceInput>;\n\n/** Prefer the human reference when both values are supplied. */\nexport function findingReferenceValue(\n reference: FindingReferenceInput,\n): string {\n const value = reference.identifier ?? reference.findingId;\n if (!value) {\n throw new Error(\"A finding identifier or findingId is required.\");\n }\n return value;\n}\n\nexport function findingReferencePath(\n reference: FindingReferenceInput,\n suffix = \"\",\n): string {\n return `/api/findings/${encodeURIComponent(findingReferenceValue(reference))}${suffix}`;\n}\n\n/**\n * Resolves only when a caller supplied an identifier. A supplied internal ID\n * is already suitable for mutation chaining and avoids an unnecessary read.\n */\nexport async function resolveFindingId(\n client: OdinClient,\n reference: FindingReferenceInput,\n): Promise<string> {\n if (reference.findingId) return reference.findingId;\n\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(reference),\n );\n return data.remediationContext?.findingId ?? data.finding.findingId;\n}\n","import { z } from \"zod\";\nimport { findingReferenceFields } from \"./finding-reference.js\";\n\nexport function findingToolInput<T extends z.ZodRawShape>(fields: T) {\n return z.object({ ...findingReferenceFields, ...fields }).refine(\n (value) => {\n const reference = value as { identifier?: string; findingId?: string };\n return Boolean(reference.identifier || reference.findingId);\n },\n { message: \"Provide either identifier or findingId.\" },\n );\n}\n\nexport const readAnnotations = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n};\n\n/**\n * Every tool that changes Odin data is marked destructive, so clients ask\n * before running it. Odin's guidance also tells agents to ask first.\n */\nexport const writeAnnotations = {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: false,\n openWorldHint: true,\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type { AttachmentUploadResponse } from \"../types.js\";\nimport { findingReferencePath } from \"./finding-reference.js\";\nimport {\n findingToolInput,\n readAnnotations,\n writeAnnotations,\n} from \"./finding-tool.js\";\n\nexport const prepareFindingAttachmentUploadInput = findingToolInput({\n filename: z.string().min(1).max(500),\n contentType: z.enum([\n \"image/png\",\n \"image/jpeg\",\n \"image/gif\",\n \"image/webp\",\n \"application/pdf\",\n \"application/zip\",\n \"application/json\",\n \"application/octet-stream\",\n \"text/plain\",\n \"text/markdown\",\n \"text/csv\",\n \"text/log\",\n ]),\n sizeBytes: z\n .number()\n .int()\n .min(1)\n .max(25 * 1024 * 1024),\n});\n\nexport const prepareFindingAttachmentUploadTool = {\n name: \"prepare_finding_attachment_upload\",\n title: \"Prepare a private discussion attachment upload\",\n description:\n \"Create a five-minute signed upload URL for a file up to 25 MiB. Requires write permission. Does not upload bytes or post a message. The caller must PUT the exact file to uploadUrl using requiredHeaders, without the Odin API key, then pass publicUrl as the attachment URL to create_finding_message. publicUrl is a private gs:// object reference. Treat signed URLs as temporary secrets.\",\n annotations: writeAnnotations,\n inputSchema: prepareFindingAttachmentUploadInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof prepareFindingAttachmentUploadInput>) => {\n const data = await client.post<AttachmentUploadResponse>(\n findingReferencePath(params, \"/thread/upload-url\"),\n {\n filename: params.filename,\n contentType: params.contentType,\n sizeBytes: params.sizeBytes,\n },\n );\n return {\n markdown:\n \"Upload URL prepared; no bytes have been uploaded. PUT the file to uploadUrl with requiredHeaders, then attach publicUrl in a discussion message. Do not send the Odin API key to the upload URL.\",\n structured: data,\n };\n },\n};\n\nexport const getFindingAttachmentUrlInput = findingToolInput({\n messageId: z.string().min(1),\n url: z\n .string()\n .url()\n .describe(\n \"Exact private attachment URL from this message's attachments array.\",\n ),\n});\n\nexport const getFindingAttachmentUrlTool = {\n name: \"get_finding_attachment_url\",\n title: \"Get a temporary attachment download URL\",\n description:\n \"Resolve a stored discussion attachment to a ten-minute signed download URL. Requires read permission and the attachment must belong to the selected finding and message. Does not download bytes or forward the Odin key to storage. Treat the returned signed URL as a temporary secret.\",\n annotations: { ...readAnnotations, openWorldHint: true },\n inputSchema: getFindingAttachmentUrlInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingAttachmentUrlInput>) => {\n const url = await client.getRedirectUrl(\n findingReferencePath(\n params,\n `/thread/attachments/${encodeURIComponent(params.messageId)}`,\n ),\n { url: params.url },\n );\n return {\n markdown:\n \"Temporary attachment download URL resolved. No bytes were downloaded; the URL is in the structured result.\",\n structured: { url },\n };\n },\n};\n\nexport const getFindingSlackUrlInput = findingToolInput({\n messageId: z.string().min(1).optional(),\n threadId: z.string().min(1).optional(),\n}).refine((value) => Boolean(value.messageId || value.threadId), {\n message: \"Provide a messageId or threadId.\",\n});\n\nexport const getFindingSlackUrlTool = {\n name: \"get_finding_slack_url\",\n title: \"Get a synced discussion's Slack permalink\",\n description:\n \"Resolve a synced message or thread to its Slack permalink. Requires read permission. Does not navigate to Slack or forward the Odin API key.\",\n annotations: { ...readAnnotations, openWorldHint: true },\n inputSchema: getFindingSlackUrlInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingSlackUrlInput>) => {\n const url = await client.getRedirectUrl(\n findingReferencePath(params, \"/thread/slack-link\"),\n { messageId: params.messageId, threadId: params.threadId },\n );\n return { markdown: `Slack permalink: ${url}`, structured: { url } };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type {\n DiscussionMessageResponse,\n FindingDiscussionResponse,\n FindingMentionablesResponse,\n FindingStatusUpdateResponse,\n FindingThreadResponse,\n} from \"../types.js\";\nimport { findingReferencePath } from \"./finding-reference.js\";\nimport {\n findingToolInput,\n readAnnotations,\n writeAnnotations,\n} from \"./finding-tool.js\";\n\nconst attachmentInput = z.object({\n type: z.enum([\"IMAGE\", \"FILE\"]),\n url: z\n .string()\n .url()\n .describe(\n \"Private gs:// URL returned by prepare_finding_attachment_upload after uploading the file.\",\n ),\n name: z.string().min(1).max(500),\n sizeBytes: z.number().int().nonnegative().optional(),\n mimeType: z.string().min(1).max(255).optional(),\n});\n\nexport const assignFindingInput = findingToolInput({\n assigneeUserId: z\n .string()\n .min(1)\n .nullable()\n .describe(\"Organisation member user ID, or null to clear assignment.\"),\n});\n\nexport const assignFindingTool = {\n name: \"assign_finding\",\n title: \"Assign a finding\",\n description:\n \"Assign a finding to an organisation member, or clear its assignment. Requires write permission. Assignment can notify the assignee by email and Slack and synchronise connected issue trackers.\",\n annotations: { ...writeAnnotations, idempotentHint: true },\n inputSchema: assignFindingInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof assignFindingInput>) => {\n const data = await client.patch<FindingStatusUpdateResponse>(\n findingReferencePath(params, \"/assignee\"),\n { assigneeUserId: params.assigneeUserId },\n );\n return {\n markdown: `Updated assignment for ${data.finding.identifier ?? data.finding.findingId}: ${params.assigneeUserId ?? \"unassigned\"}.`,\n structured: { finding: data.finding },\n };\n },\n};\n\nexport const getFindingDiscussionInput = findingToolInput({\n limit: z.number().int().min(1).max(100).optional(),\n offset: z.number().int().nonnegative().optional(),\n before: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe(\n \"Only messages before this timestamp, in the API's stored timestamp units.\",\n ),\n});\n\nexport const getFindingDiscussionTool = {\n name: \"get_finding_discussion\",\n title: \"Read finding discussions\",\n description:\n \"Read native and Slack-synchronised conversation blocks for a finding. Each block paginates independently. API keys have no personal read or unread state.\",\n annotations: readAnnotations,\n inputSchema: getFindingDiscussionInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingDiscussionInput>) => {\n const data = await client.get<FindingDiscussionResponse>(\n findingReferencePath(params, \"/thread\"),\n { limit: params.limit, offset: params.offset, before: params.before },\n );\n const markdown = data.blocks.length\n ? data.blocks\n .map((block) =>\n [\n `## ${block.thread.type} discussion ${block.thread.threadId} (${block.thread.status})`,\n `More messages: ${block.hasMore}.`,\n ...block.messages.map(\n (message) =>\n `### ${message.author.displayName} (${message.messageId})\\n${message.bodyMd}`,\n ),\n ].join(\"\\n\\n\"),\n )\n .join(\"\\n\\n\")\n : \"No discussions exist for this finding.\";\n return { markdown, structured: data };\n },\n};\n\nexport const createFindingMessageInput = findingToolInput({\n bodyMd: z\n .string()\n .min(1)\n .max(50_000)\n .describe(\n \"Markdown body. Mention syntax: @[Label](USER:id), @[Label](OPERATOR:id), @[ODI-12](FINDING:id), or @[Pentest](PENTEST:id); use list_finding_mentionables to discover IDs.\",\n ),\n threadId: z\n .string()\n .min(1)\n .optional()\n .describe(\"Existing block to reply to; omit for the native discussion.\"),\n attachments: z.array(attachmentInput).max(20).optional(),\n});\n\nexport const createFindingMessageTool = {\n name: \"create_finding_message\",\n title: \"Post a finding discussion message\",\n description:\n \"Post a Markdown discussion message. Requires write permission and acts as the API key creator. May send email, mention notifications and Slack messages. Repeating a call creates another message.\",\n annotations: writeAnnotations,\n inputSchema: createFindingMessageInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof createFindingMessageInput>) => {\n const data = await client.post<DiscussionMessageResponse>(\n findingReferencePath(params, \"/thread\"),\n {\n bodyMd: params.bodyMd,\n threadId: params.threadId,\n attachments: params.attachments,\n },\n );\n return {\n markdown: `Posted message ${data.message.messageId}.`,\n structured: data,\n };\n },\n};\n\nexport const editFindingMessageInput = findingToolInput({\n messageId: z.string().min(1),\n bodyMd: z.string().min(1).max(50_000),\n});\n\nexport const editFindingMessageTool = {\n name: \"edit_finding_message\",\n title: \"Edit a finding discussion message\",\n description:\n \"Edit a message authored by the API key creator. Requires write permission. New mentions can send notifications and the edit can synchronise to Slack.\",\n annotations: writeAnnotations,\n inputSchema: editFindingMessageInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof editFindingMessageInput>) => {\n const data = await client.patch<DiscussionMessageResponse>(\n findingReferencePath(\n params,\n `/thread/${encodeURIComponent(params.messageId)}`,\n ),\n { bodyMd: params.bodyMd },\n );\n return {\n markdown: `Edited message ${data.message.messageId}.`,\n structured: data,\n };\n },\n};\n\nexport const deleteFindingMessageInput = findingToolInput({\n messageId: z.string().min(1),\n});\n\nexport const deleteFindingMessageTool = {\n name: \"delete_finding_message\",\n title: \"Delete a finding discussion message\",\n description:\n \"Delete a message authored by the API key creator. Requires write permission and may delete its Slack mirror. Confirm the user's intent before deleting.\",\n annotations: writeAnnotations,\n inputSchema: deleteFindingMessageInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof deleteFindingMessageInput>) => {\n const data = await client.delete<FindingThreadResponse>(\n findingReferencePath(\n params,\n `/thread/${encodeURIComponent(params.messageId)}`,\n ),\n );\n return {\n markdown: `Deleted message ${params.messageId}.`,\n structured: data,\n };\n },\n};\n\nexport const updateFindingDiscussionInput = findingToolInput({\n action: z.enum([\"resolve\", \"reopen\"]),\n});\n\nexport const updateFindingDiscussionTool = {\n name: \"update_finding_discussion\",\n title: \"Resolve or reopen the native discussion\",\n description:\n \"Resolve or reopen a finding's native discussion. Requires write permission and may synchronise the status to Slack. This does not change the finding's security status.\",\n annotations: writeAnnotations,\n inputSchema: updateFindingDiscussionInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingDiscussionInput>) => {\n const data = await client.post<FindingThreadResponse>(\n findingReferencePath(params, \"/thread/resolve\"),\n { action: params.action },\n );\n return {\n markdown: `Discussion status: ${data.thread?.status ?? params.action}.`,\n structured: data,\n };\n },\n};\n\nexport const listFindingMentionablesInput = findingToolInput({\n q: z.string().trim().max(100).optional(),\n});\n\nexport const listFindingMentionablesTool = {\n name: \"list_finding_mentionables\",\n title: \"Find people and entities to mention\",\n description:\n \"Find authorised people, findings and pentests that can be mentioned in a finding discussion. Returns up to eight people, eight findings and eight pentests matching q.\",\n annotations: readAnnotations,\n inputSchema: listFindingMentionablesInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingMentionablesInput>) => {\n const data = await client.get<FindingMentionablesResponse>(\n findingReferencePath(params, \"/thread/mentionables\"),\n { q: params.q },\n );\n return {\n markdown:\n data.mentionables\n .map((item) => `- ${item.label}: ${item.id}`)\n .join(\"\\n\") || \"No matching mentionables.\",\n structured: data,\n };\n },\n};\n\nexport const disconnectFindingDiscussionInput = findingToolInput({\n threadId: z.string().min(1),\n});\n\nexport const disconnectFindingDiscussionTool = {\n name: \"disconnect_finding_slack_thread\",\n title: \"Disconnect a synced Slack thread\",\n description:\n \"Disconnect a finding's SLACK_SYNC discussion block while preserving imported messages. Stops future synchronisation and may post a confirmation to Slack. Requires write permission. Confirm the user's intent before disconnecting.\",\n annotations: writeAnnotations,\n inputSchema: disconnectFindingDiscussionInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof disconnectFindingDiscussionInput>) => {\n const data = await client.post<FindingThreadResponse>(\n findingReferencePath(params, \"/thread/disconnect\"),\n { threadId: params.threadId },\n );\n return {\n markdown: `Disconnected Slack discussion ${params.threadId}. Existing messages are preserved.`,\n structured: data,\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type {\n FindingPullRequestsResponse,\n UnlinkFindingPullRequestResponse,\n} from \"../types.js\";\nimport { findingReferencePath } from \"./finding-reference.js\";\nimport {\n findingToolInput,\n readAnnotations,\n writeAnnotations,\n} from \"./finding-tool.js\";\n\nexport const listFindingPullRequestsInput = findingToolInput({\n query: z\n .string()\n .optional()\n .describe(\"PR number, title search, owner/repo#123, or GitHub PR URL.\"),\n page: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),\n});\n\nexport const listFindingPullRequestsTool = {\n name: \"list_finding_pull_requests\",\n title: \"Find accessible pull requests to link\",\n description:\n \"Search accessible GitHub pull requests for this finding. Defaults to the finding's repository, otherwise its connected GitHub organisation. Results are advisory; link_finding_pull_request verifies the chosen PR. Requires read permission and a configured GitHub integration.\",\n annotations: { ...readAnnotations, openWorldHint: true },\n inputSchema: listFindingPullRequestsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingPullRequestsInput>) => {\n const data = await client.get<FindingPullRequestsResponse>(\n findingReferencePath(params, \"/link-pr\"),\n { query: params.query, page: params.page },\n );\n return {\n markdown: [\n `Accessible pull requests (${data.repository ?? \"connected organisation\"}); more pages: ${data.pagination.hasMore}.`,\n ...data.pullRequests.map(\n (pr) =>\n `- ${pr.repo}#${pr.number}: ${pr.title} (${pr.state}) ${pr.url}`,\n ),\n ].join(\"\\n\"),\n structured: data,\n };\n },\n};\n\nexport const unlinkFindingPullRequestInput = findingToolInput({\n prNodeId: z\n .string()\n .min(1)\n .describe(\n \"Stored prNodeId of a manually linked pull request from get_finding.\",\n ),\n});\n\nexport const unlinkFindingPullRequestTool = {\n name: \"unlink_finding_pull_request\",\n title: \"Unlink a manually linked pull request\",\n description:\n \"Remove a manual finding-to-PR link. Requires write permission. Automatic links cannot be removed with this operation. Does not modify, close, merge or delete the GitHub PR. Confirm the user's intent before removing the link.\",\n annotations: { ...writeAnnotations, openWorldHint: false },\n inputSchema: unlinkFindingPullRequestInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof unlinkFindingPullRequestInput>) => {\n const data = await client.delete<UnlinkFindingPullRequestResponse>(\n findingReferencePath(params, \"/link-pr\"),\n { prNodeId: params.prNodeId },\n );\n return {\n markdown: `Removed manual pull request link ${params.prNodeId} from ${data.finding.identifier ?? data.finding.findingId}.`,\n structured: data,\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport type {\n FindingAssetsResponse,\n FindingRepositoriesResponse,\n FindingStatsResponse,\n FindingsExportResponse,\n RetestSummariesResponse,\n ThreadSummariesResponse,\n} from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\nimport { findingFilterFields, serializeFindingQuery } from \"./list-findings.js\";\n\nexport const getFindingStatsInput = z.object({\n pentestId: z.string().optional(),\n});\n\nexport const getFindingStatsTool = {\n name: \"get_finding_stats\",\n title: \"Get finding statistics\",\n description:\n \"Read aggregate finding totals by severity and status, optionally scoped to a pentest. These totals do not apply other finding list filters.\",\n annotations: readAnnotations,\n inputSchema: getFindingStatsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingStatsInput>) => {\n const data = await client.get<FindingStatsResponse>(\n \"/api/findings/stats\",\n params,\n );\n return {\n markdown: `Total findings: ${data.stats.total}.\\n${JSON.stringify(data.stats, null, 2)}`,\n structured: data,\n };\n },\n};\n\nexport const listFindingRepositoriesTool = {\n name: \"list_finding_repositories\",\n title: \"List repositories with findings\",\n description:\n \"List organisation repositories available for the repository filter in list_findings.\",\n annotations: readAnnotations,\n inputSchema: z.object({}),\n handler: (client: OdinClient) => async () => {\n const data = await client.get<FindingRepositoriesResponse>(\n \"/api/findings/repositories\",\n );\n return {\n markdown:\n data.repositories.map((repository) => `- ${repository}`).join(\"\\n\") ||\n \"No repositories with findings.\",\n structured: data,\n };\n },\n};\n\nexport const listFindingAssetsTool = {\n name: \"list_finding_assets\",\n title: \"List assets referenced by findings\",\n description:\n \"List organisation assets referenced by findings for use with the assetId filter. This differs from listing all attack-surface assets.\",\n annotations: readAnnotations,\n inputSchema: z.object({}),\n handler: (client: OdinClient) => async () => {\n const data = await client.get<FindingAssetsResponse>(\n \"/api/findings/assets\",\n );\n return {\n markdown:\n data.assets\n .map((asset) => `- ${asset.value} (${asset.type}, ${asset.assetId})`)\n .join(\"\\n\") || \"No assets referenced by findings.\",\n structured: data,\n };\n },\n};\n\nexport const exportFindingsInput = z.object(findingFilterFields);\n\nexport const exportFindingsTool = {\n name: \"export_findings\",\n title: \"Export findings and revision content\",\n description:\n \"Export findings matching the supplied filters, including revision Markdown and affected assets. The API returns all matching rows without pagination; narrow the filters for large organisations. This reads data and creates no file.\",\n annotations: readAnnotations,\n inputSchema: exportFindingsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof exportFindingsInput>) => {\n const data = await client.get<FindingsExportResponse>(\n \"/api/findings/export\",\n serializeFindingQuery(params),\n );\n return {\n markdown: `Exported ${data.findings.length} matching finding(s). Revision content is in the structured findings result.`,\n structured: data,\n };\n },\n};\n\nexport const findingSummariesInput = z.object({\n findingIds: z\n .array(z.string().trim().min(1))\n .max(200)\n .describe(\n \"Up to 200 internal finding IDs from list_findings. Human identifiers are not accepted by these batch endpoints.\",\n ),\n});\n\nexport const getFindingRetestSummariesTool = {\n name: \"get_finding_retest_summaries\",\n title: \"Get latest retests for several findings\",\n description:\n \"Read the latest retest summary for up to 200 finding IDs. Requires read permission and the organisation's retest capability; never starts a retest or spends credits. Missing entries have no latest retest.\",\n annotations: readAnnotations,\n inputSchema: findingSummariesInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof findingSummariesInput>) => {\n const data = await client.post<RetestSummariesResponse>(\n \"/api/findings/retest-summaries\",\n params,\n );\n return {\n markdown: `Latest retests available for ${Object.keys(data.summaries).length} finding(s).\\n${JSON.stringify(data.summaries, null, 2)}`,\n structured: data,\n };\n },\n};\n\nexport const getFindingDiscussionSummariesTool = {\n name: \"get_finding_discussion_summaries\",\n title: \"Get discussion summaries for several findings\",\n description:\n \"Read message counts and unresolved discussion state for up to 200 finding IDs. API keys have no personal unread state; unreadCount is zero for key-authenticated calls.\",\n annotations: readAnnotations,\n inputSchema: findingSummariesInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof findingSummariesInput>) => {\n const data = await client.post<ThreadSummariesResponse>(\n \"/api/findings/thread-summaries\",\n params,\n );\n return {\n markdown: `Discussion summaries available for ${Object.keys(data.summaries).length} finding(s).\\n${JSON.stringify(data.summaries, null, 2)}`,\n structured: data,\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingsList } from \"../formatters/markdown.js\";\nimport type { FindingsListResponse } from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nconst severityInput = z.enum([\n \"CRITICAL\",\n \"HIGH\",\n \"MEDIUM\",\n \"LOW\",\n \"INFORMATIONAL\",\n]);\nconst statusInput = z.enum([\n \"REPORTED\",\n \"ACKNOWLEDGED\",\n \"MITIGATING\",\n \"OPEN_FOR_RETEST\",\n \"NEEDS_REVISION\",\n \"FIXED_AND_RETESTED\",\n]);\nconst sourceInput = z.enum([\n \"MANUAL\",\n \"WATCHDOG\",\n \"IMPORT\",\n \"AGENT\",\n \"THREAT_MODEL\",\n]);\nconst confirmationTypeInput = z.enum([\"static\", \"partial\", \"exploited\"]);\nconst assigneeInput = z.string().trim().min(1).max(500);\n\nexport const findingFilterFields = {\n pentestId: z.string().optional().describe(\"Filter to a single pentest.\"),\n assetId: z\n .string()\n .optional()\n .describe(\n \"Filter to findings whose latest revision references this asset.\",\n ),\n repository: z\n .string()\n .optional()\n .describe(\"Filter to a source repository such as owner/name.\"),\n severity: z.union([severityInput, z.array(severityInput).min(1)]).optional(),\n status: z.union([statusInput, z.array(statusInput).min(1)]).optional(),\n excludeStatuses: z\n .union([statusInput, z.array(statusInput).min(1)])\n .optional()\n .describe(\"Exclude these client-visible statuses.\"),\n source: z.union([sourceInput, z.array(sourceInput).min(1)]).optional(),\n confirmationType: z\n .union([confirmationTypeInput, z.array(confirmationTypeInput).min(1)])\n .optional(),\n assignee: z\n .union([assigneeInput, z.array(assigneeInput).min(1)])\n .refine((value) => String(value).length <= 500, {\n message: \"Combined assignee filter must be at most 500 characters.\",\n })\n .optional()\n .describe(\n \"Client user ID(s), or __unassigned__ to include findings without an assignee.\",\n ),\n cvssMin: z.number().min(0).max(10).optional(),\n cvssMax: z.number().min(0).max(10).optional(),\n hideDormant: z\n .boolean()\n .optional()\n .describe(\"Hide findings on dormant assets.\"),\n search: z\n .string()\n .optional()\n .describe(\"Free-text search on finding titles and human identifiers.\"),\n};\n\nexport const listFindingsInput = z.object({\n ...findingFilterFields,\n lean: z\n .boolean()\n .optional()\n .describe(\n \"Exclude heavy evidence from list results; details stay available.\",\n ),\n stats: z\n .boolean()\n .optional()\n .describe(\"Include aggregate statistics (defaults to true).\"),\n sortBy: z.enum([\"severity\", \"createdAt\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(200).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport function serializeFindingQuery(\n params: Record<string, string | number | boolean | string[] | undefined>,\n): Record<string, string | number | undefined> {\n return Object.fromEntries(\n Object.entries(params).map(([key, value]) => [\n key,\n Array.isArray(value)\n ? value.join(\",\")\n : typeof value === \"boolean\"\n ? String(value)\n : value,\n ]),\n );\n}\n\nexport const listFindingsTool = {\n name: \"list_findings\",\n title: \"List findings\",\n description:\n \"List security findings for the organisation. Filter by pentest, asset, repository, severity, client-visible status, source, confirmation type, assignee, CVSS range, or dormant assets. Search titles or human identifiers such as ODI-12.\",\n annotations: readAnnotations,\n inputSchema: listFindingsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof listFindingsInput>) => {\n const data = await client.get<FindingsListResponse>(\n \"/api/findings\",\n serializeFindingQuery(params),\n );\n return {\n markdown: formatFindingsList(data),\n structured: {\n findings: data.findings,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import type {\n Asset,\n AssetDetailResponse,\n AssetsListResponse,\n FindingDetailResponse,\n FindingStatus,\n FindingSummary,\n FindingsListResponse,\n LinkFindingPullRequestResponse,\n Pentest,\n PentestDetailResponse,\n PentestListResponse,\n Retest,\n RetestHistoryResponse,\n} from \"../types.js\";\n\nconst STATUS_LABELS: Record<FindingStatus, string> = {\n REPORTED: \"Reported\",\n ACKNOWLEDGED: \"Risk accepted\",\n MITIGATING: \"Fixing\",\n OPEN_FOR_RETEST: \"Open for Retest\",\n NEEDS_REVISION: \"Needs Revision\",\n FIXED_AND_RETESTED: \"Fixed & Retested\",\n};\n\nconst RETEST_STATUS_LABELS: Record<Retest[\"status\"], string> = {\n QUEUED: \"Queued\",\n RUNNING: \"Running\",\n COMPLETED: \"Completed\",\n FAILED: \"Failed\",\n SKIPPED: \"Skipped\",\n};\n\nexport function formatFindingsList(data: FindingsListResponse): string {\n const { findings, pagination } = data;\n if (findings.length === 0) {\n return \"No findings match the supplied filters.\";\n }\n\n const rows = findings.map((f) => {\n const severity = `${f.severity.label} (CVSS ${f.severity.cvss.toFixed(1)})`;\n return `| ${f.identifier ?? \"—\"} | ${f.findingId} | ${escapePipes(f.title)} | ${severity} | ${formatStatus(f.status)} | ${formatDate(f.createdAt)} |`;\n });\n\n return [\n `Showing ${findings.length} of ${pagination.total} finding(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Identifier | Internal ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatFindingDetail(data: FindingDetailResponse): string {\n const { finding, revision } = data;\n const context = data.remediationContext;\n const identifier = context?.identifier ?? finding.identifier ?? null;\n const displayIdentifier = identifier ?? finding.findingId;\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n const header = [\n `# ${escapePipes(displayIdentifier)} — ${escapePipes(finding.title)}`,\n \"\",\n `- **Finding:** ${displayIdentifier}`,\n `- **Internal ID:** ${finding.findingId}`,\n `- **Severity:** ${severity}`,\n `- **Status:** ${formatStatus(finding.status)}`,\n `- **Repository:** ${context?.repository ?? finding.repository ?? \"Not recorded\"}`,\n `- **Pentest:** ${finding.context?.pentestId ?? \"standalone\"}`,\n `- **Created:** ${formatDate(finding.createdAt)}`,\n `- **Updated:** ${formatDate(finding.updatedAt)}`,\n ];\n\n const content = context\n ? {\n revisionNumber: context.revision.revisionNumber,\n summary: context.revision.summary,\n description: context.revision.description,\n impact: context.revision.impact,\n details: context.revision.details,\n remediation: context.revision.remediation,\n affectedAssets: context.revision.affectedAssets,\n codeReference: context.revision.codeReference,\n }\n : revision\n ? {\n revisionNumber: revision.revisionNumber,\n summary: revision.content.summaryMd,\n description: revision.content.descriptionMd,\n impact: revision.content.businessImpactMd,\n details: revision.content.detailsMd,\n remediation: revision.content.remediationMd,\n affectedAssets: revision.content.affectedAssets ?? [],\n codeReference: null,\n }\n : null;\n\n const sections: string[] = [];\n if (!content) {\n sections.push(\"_No revision content available._\");\n } else {\n sections.push(`_Revision ${content.revisionNumber ?? \"unknown\"}_`);\n if (content.summary) sections.push(\"## Summary\", content.summary);\n if (content.description)\n sections.push(\"## Description\", content.description);\n if (content.impact) sections.push(\"## Business Impact\", content.impact);\n if (content.details) sections.push(\"## Details\", content.details);\n if (content.remediation)\n sections.push(\"## Remediation\", content.remediation);\n if (content.codeReference) {\n sections.push(\n \"## Code reference\",\n `- ${content.codeReference.file}:${content.codeReference.line}`,\n );\n if (content.codeReference.snippet) {\n sections.push(\"```\", content.codeReference.snippet, \"```\");\n }\n }\n if (content.affectedAssets?.length) {\n sections.push(\n \"## Affected Assets\",\n content.affectedAssets\n .map((asset) => `- ${asset.display} (${asset.assetId})`)\n .join(\"\\n\"),\n );\n }\n }\n\n if (context) {\n sections.push(\n \"## Remediation workflow\",\n `- **Suggested branch:** ${context.suggestedBranch ?? \"Not available\"}`,\n `- **Ready for a user-created PR:** ${yesNo(context.workflow.readyForUserCreatedPullRequest)}`,\n `- **GitHub connected:** ${yesNo(context.workflow.githubConnected)}`,\n `- **Automatic retests:** ${context.workflow.automaticRetestsReady ? \"Ready after merge\" : \"Not ready\"}`,\n );\n if (context.workflow.blockers.length) {\n sections.push(\n \"### Blockers\",\n context.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\"),\n );\n }\n if (context.workflow.nextSteps.length) {\n sections.push(\n \"### Next steps\",\n context.workflow.nextSteps.map((step) => `- ${step}`).join(\"\\n\"),\n );\n }\n\n if (context.linkedPullRequests.length) {\n sections.push(\n \"## Linked GitHub pull requests\",\n context.linkedPullRequests\n .map(\n (pullRequest) =>\n `- [#${pullRequest.number}](${pullRequest.url}) ${escapePipes(pullRequest.title)} — ${pullRequest.state}, linked via ${pullRequest.linkedVia}`,\n )\n .join(\"\\n\"),\n );\n } else {\n sections.push(\"## Linked GitHub pull requests\", \"_None linked._\");\n }\n\n sections.push(\"## Latest retest\");\n if (context.latestRetest.state === \"unavailable\") {\n sections.push(`_${context.latestRetest.reason}_`);\n } else if (context.latestRetest.state === \"none\") {\n sections.push(\"_No retest history is available yet._\");\n } else {\n sections.push(formatRetestSummary(context.latestRetest.retest));\n }\n }\n\n return [...header, \"\", ...sections].join(\"\\n\");\n}\n\nexport function formatFindingStatusUpdate(finding: FindingSummary): string {\n const identifier = finding.identifier ?? finding.findingId;\n return [\n `Updated finding **${identifier}** (internal ID ${finding.findingId}) status to **${formatStatus(finding.status)}**.`,\n `Title: ${finding.title}`,\n ].join(\"\\n\");\n}\n\nexport function formatFindingPullRequestLink(\n data: LinkFindingPullRequestResponse,\n): string {\n const identifier = data.finding.identifier ?? data.finding.findingId;\n const pullRequest = data.pullRequest;\n const readiness = data.workflow.automaticRetestsReady\n ? \"Automatic retesting is ready after this PR merges.\"\n : \"Automatic retesting is not currently ready; review the blockers below.\";\n const blockers = data.workflow.blockers.length\n ? `\\n${data.workflow.blockers.map((blocker) => `- ${blocker}`).join(\"\\n\")}`\n : \"\";\n\n return [\n `Linked existing GitHub pull request [#${pullRequest.number}](${pullRequest.url}) to **${identifier}**.`,\n `Repository: ${pullRequest.repo}; state: ${pullRequest.state}; finding status: ${formatStatus(data.finding.status)}.`,\n \"Odin did not create, modify, merge, or push this pull request.\",\n readiness,\n blockers ? `Workflow blockers:${blockers}` : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\nexport function formatFindingRetests(data: RetestHistoryResponse): string {\n if (data.state === \"none\" || data.retests.length === 0) {\n return \"No retest history is available for this finding.\";\n }\n\n return [\n `Retest history (${data.retests.length}):`,\n \"\",\n \"| Retest | Status | Trigger | Verdict | Created | Completed |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...data.retests.map(\n (retest) =>\n `| ${retest.retestId} | ${RETEST_STATUS_LABELS[retest.status]} | ${retest.trigger} | ${retest.combinedVerdict ?? \"—\"} | ${formatDate(retest.createdAt)} | ${formatDate(retest.completedAt ?? 0)} |`,\n ),\n \"\",\n \"Queued and running entries are still in progress; completed, skipped, and failed entries are final for that run.\",\n ].join(\"\\n\");\n}\n\nexport function formatPentestsList(data: PentestListResponse): string {\n if (data.pentests.length === 0) return \"No pentests found.\";\n const rows = data.pentests.map(formatPentestRow);\n return [\n \"| Pentest ID | Name | Status | Start | End | Scope |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatPentestDetail(data: PentestDetailResponse): string {\n const p = data.pentest;\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope = p.scope.length\n ? p.scope.map((s) => `- ${s}`).join(\"\\n\")\n : \"_No scope defined._\";\n return [\n `# ${p.name}`,\n \"\",\n `- **ID:** ${p.pentestId}`,\n `- **Status:** ${p.status}`,\n `- **Start:** ${formatDate(p.startAt)}`,\n `- **End:** ${end}`,\n `- **Created:** ${formatDate(p.createdAt)}`,\n \"\",\n \"## Scope\",\n \"\",\n scope,\n ].join(\"\\n\");\n}\n\nexport function formatFindingsForAsset(findings: FindingSummary[]): string {\n if (findings.length === 0) return \"_No findings reference this asset._\";\n return [\n `## Findings (${findings.length})`,\n \"\",\n \"| Identifier | Internal ID | Title | Severity | Status | Created |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...findings.map(formatFindingRow),\n ].join(\"\\n\");\n}\n\nexport function formatAssetsList(data: AssetsListResponse): string {\n const { assets, pagination } = data;\n if (assets.length === 0) return \"No assets match the supplied filters.\";\n const rows = assets.map(formatAssetRow);\n return [\n `Showing ${assets.length} of ${pagination.total} asset(s). Offset ${pagination.offset}, limit ${pagination.limit}. hasMore=${pagination.hasMore}.`,\n \"\",\n \"| Asset ID | Type | Value | Source | Status | Last seen |\",\n \"| --- | --- | --- | --- | --- | --- |\",\n ...rows,\n ].join(\"\\n\");\n}\n\nexport function formatAssetDetail(data: AssetDetailResponse): string {\n const { asset, findings } = data;\n const header = [\n `# ${asset.value}`,\n \"\",\n `- **ID:** ${asset.assetId}`,\n `- **Type:** ${asset.type}`,\n `- **Source:** ${asset.source}`,\n `- **Status:** ${asset.status}`,\n `- **Last seen:** ${formatDate(asset.lastSeenAt)}`,\n `- **Created:** ${formatDate(asset.createdAt)}`,\n ];\n\n return [...header, \"\", formatFindingsForAsset(findings)].join(\"\\n\");\n}\n\nfunction formatFindingRow(finding: FindingSummary): string {\n const severity = `${finding.severity.label} (CVSS ${finding.severity.cvss.toFixed(1)})`;\n return `| ${finding.identifier ?? \"—\"} | ${finding.findingId} | ${escapePipes(finding.title)} | ${severity} | ${formatStatus(finding.status)} | ${formatDate(finding.createdAt)} |`;\n}\n\nfunction formatPentestRow(p: Pentest): string {\n const end = p.endAt ? formatDate(p.endAt) : \"open\";\n const scope =\n p.scope.slice(0, 3).join(\", \") + (p.scope.length > 3 ? \", …\" : \"\");\n return `| ${p.pentestId} | ${escapePipes(p.name)} | ${p.status} | ${formatDate(p.startAt)} | ${end} | ${escapePipes(scope)} |`;\n}\n\nfunction formatAssetRow(a: Asset): string {\n return `| ${a.assetId} | ${a.type} | ${escapePipes(a.value)} | ${a.source} | ${a.status} | ${formatDate(a.lastSeenAt)} |`;\n}\n\nfunction formatRetestSummary(retest: {\n status: Retest[\"status\"];\n combinedVerdict: Retest[\"combinedVerdict\"];\n statusAction: Retest[\"statusAction\"];\n trigger: Retest[\"trigger\"];\n createdAt: number;\n completedAt: number | null;\n}): string {\n return [\n `- **Status:** ${RETEST_STATUS_LABELS[retest.status]}`,\n `- **Trigger:** ${retest.trigger}`,\n `- **Verdict:** ${retest.combinedVerdict ?? \"Not available yet\"}`,\n `- **Finding action:** ${retest.statusAction ?? \"None\"}`,\n `- **Created:** ${formatDate(retest.createdAt)}`,\n `- **Completed:** ${formatDate(retest.completedAt ?? 0)}`,\n ].join(\"\\n\");\n}\n\nfunction formatStatus(status: FindingStatus): string {\n return STATUS_LABELS[status] ?? status;\n}\n\nfunction yesNo(value: boolean): string {\n return value ? \"Yes\" : \"No\";\n}\n\nfunction escapePipes(value: string): string {\n return value.replace(/\\|/g, \"\\\\|\");\n}\n\nfunction formatDate(epoch: number): string {\n if (!epoch) return \"—\";\n // Odin stores timestamps as milliseconds for documents and seconds for some fields\n // (pentest startAt/endAt are seconds). Heuristic: values < 10^12 are seconds.\n const ms = epoch < 1e12 ? epoch * 1000 : epoch;\n return new Date(ms).toISOString().slice(0, 10);\n}\n","import {\n followFindingTool,\n requestFindingRetestTool,\n} from \"./finding-actions.js\";\nimport {\n getFindingAttachmentUrlTool,\n getFindingSlackUrlTool,\n prepareFindingAttachmentUploadTool,\n} from \"./finding-attachments.js\";\nimport {\n assignFindingTool,\n createFindingMessageTool,\n deleteFindingMessageTool,\n disconnectFindingDiscussionTool,\n editFindingMessageTool,\n getFindingDiscussionTool,\n listFindingMentionablesTool,\n updateFindingDiscussionTool,\n} from \"./finding-collaboration.js\";\nimport {\n listFindingPullRequestsTool,\n unlinkFindingPullRequestTool,\n} from \"./finding-pull-requests.js\";\nimport {\n exportFindingsTool,\n getFindingDiscussionSummariesTool,\n getFindingRetestSummariesTool,\n getFindingStatsTool,\n listFindingAssetsTool,\n listFindingRepositoriesTool,\n} from \"./findings-overview.js\";\n\nexport const additionalFindingTools = [\n getFindingStatsTool,\n listFindingRepositoriesTool,\n listFindingAssetsTool,\n exportFindingsTool,\n getFindingRetestSummariesTool,\n getFindingDiscussionSummariesTool,\n assignFindingTool,\n getFindingDiscussionTool,\n createFindingMessageTool,\n editFindingMessageTool,\n deleteFindingMessageTool,\n updateFindingDiscussionTool,\n listFindingMentionablesTool,\n disconnectFindingDiscussionTool,\n prepareFindingAttachmentUploadTool,\n getFindingAttachmentUrlTool,\n getFindingSlackUrlTool,\n listFindingPullRequestsTool,\n unlinkFindingPullRequestTool,\n followFindingTool,\n requestFindingRetestTool,\n];\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetDetail } from \"../formatters/markdown.js\";\nimport type { AssetDetailResponse } from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const getAssetInput = z.object({\n assetId: z.string().min(1),\n});\n\nexport const getAssetTool = {\n name: \"get_asset\",\n title: \"Get asset\",\n description:\n \"Fetch a single attack-surface asset by ID, including the findings that reference it in their latest revision.\",\n annotations: readAnnotations,\n inputSchema: getAssetInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getAssetInput>) => {\n const data = await client.get<AssetDetailResponse>(\n `/api/assets/${encodeURIComponent(params.assetId)}`,\n );\n return {\n markdown: formatAssetDetail(data),\n structured: { asset: data.asset, findings: data.findings },\n };\n },\n};\n","import type { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingDetail } from \"../formatters/markdown.js\";\nimport type { FindingDetailResponse } from \"../types.js\";\nimport {\n findingReferenceInput,\n findingReferencePath,\n} from \"./finding-reference.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const getFindingInput = findingReferenceInput;\n\nexport const getFindingTool = {\n name: \"get_finding\",\n title: \"Get finding remediation context\",\n description:\n \"Fetch a finding by human identifier such as ODI-12 (preferred) or by internal findingId. Returns bounded remediation context, the suggested branch name, linked GitHub pull requests, workflow readiness, and retest state. Odin only provides context and can link an existing PR; it never creates commits, pushes branches, or creates pull requests.\",\n annotations: readAnnotations,\n inputSchema: getFindingInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getFindingInput>) => {\n const data = await client.get<FindingDetailResponse>(\n findingReferencePath(params),\n );\n return {\n markdown: formatFindingDetail(data),\n structured: {\n finding: data.remediationContext ?? data.finding,\n revision: data.remediationContext?.revision ?? data.revision,\n remediationContext: data.remediationContext ?? null,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { OdinApiError } from \"../client.js\";\nimport { formatFindingRetests } from \"../formatters/markdown.js\";\nimport type { RetestHistoryResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const getFindingRetestsInput = z\n .object(findingReferenceFields)\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const getFindingRetestsTool = {\n name: \"get_finding_retests\",\n title: \"Get finding retest history\",\n description:\n \"Read the retest history for a finding by human identifier such as ODI-12 or internal findingId. Distinguishes no history, queued or running, completed, skipped, failed, and gated/unavailable results. This tool never triggers a retest or spends credits.\",\n annotations: readAnnotations,\n inputSchema: getFindingRetestsInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof getFindingRetestsInput>) => {\n try {\n const data = await client.get<RetestHistoryResponse>(\n findingReferencePath(params, \"/retests\"),\n );\n return {\n markdown: formatFindingRetests(data),\n structured: {\n state: data.state,\n retests: data.retests,\n },\n };\n } catch (error) {\n if (\n error instanceof OdinApiError &&\n error.status === 404 &&\n isRetestHistoryUnavailable(error.body)\n ) {\n const reason =\n \"Retest history is gated or unavailable for this organisation.\";\n return {\n markdown: reason,\n structured: { state: \"unavailable\", reason, retests: [] },\n };\n }\n throw error;\n }\n },\n};\n\nfunction isRetestHistoryUnavailable(body: unknown): boolean {\n return (\n typeof body === \"object\" &&\n body !== null &&\n \"code\" in body &&\n (body as { code?: unknown }).code === \"RETEST_HISTORY_UNAVAILABLE\"\n );\n}\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestDetail } from \"../formatters/markdown.js\";\nimport type { PentestDetailResponse } from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const getPentestInput = z.object({\n pentestId: z.string().min(1),\n});\n\nexport const getPentestTool = {\n name: \"get_pentest\",\n title: \"Get pentest\",\n description:\n \"Fetch a single pentest by ID, including status, scheduled window, and in-scope targets.\",\n annotations: readAnnotations,\n inputSchema: getPentestInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof getPentestInput>) => {\n const data = await client.get<PentestDetailResponse>(\n `/api/pentests/${encodeURIComponent(params.pentestId)}`,\n );\n return {\n markdown: formatPentestDetail(data),\n structured: { pentest: data.pentest },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingPullRequestLink } from \"../formatters/markdown.js\";\nimport type { LinkFindingPullRequestResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n findingReferencePath,\n} from \"./finding-reference.js\";\nimport { writeAnnotations } from \"./finding-tool.js\";\n\nexport const linkFindingPullRequestInput = z\n .object({\n ...findingReferenceFields,\n url: z\n .string()\n .url()\n .describe(\"Existing GitHub pull request URL to link to the finding.\"),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const linkFindingPullRequestTool = {\n name: \"link_finding_pull_request\",\n title: \"Link an existing GitHub pull request\",\n description:\n \"Link an existing accessible GitHub pull request to a finding identified by ODI-12 or findingId. This operation is idempotent. It does not create, modify, merge, or push a pull request, commit, or branch, and it requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Ask the user before linking.\",\n annotations: { ...writeAnnotations, idempotentHint: true },\n inputSchema: linkFindingPullRequestInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof linkFindingPullRequestInput>) => {\n const data = await client.post<LinkFindingPullRequestResponse>(\n findingReferencePath(params, \"/link-pr\"),\n { url: params.url },\n );\n return {\n markdown: formatFindingPullRequestLink(data),\n structured: {\n finding: data.finding,\n pullRequest: data.pullRequest,\n linkSource: data.linkSource,\n workflow: data.workflow,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatAssetsList } from \"../formatters/markdown.js\";\nimport type { AssetsListResponse } from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const listAssetsInput = z.object({\n type: z.enum([\"DOMAIN\", \"SUBDOMAIN\", \"IP\"]).optional(),\n source: z.enum([\"MANUAL\", \"RECON\", \"IMPORT\", \"AGENT\"]).optional(),\n status: z\n .string()\n .optional()\n .describe(\n \"Dynamic asset status set by recon (e.g. ACTIVE, INACTIVE, TLS_ERROR).\",\n ),\n search: z.string().optional(),\n stats: z\n .boolean()\n .optional()\n .describe(\"Include aggregate statistics (defaults to true).\"),\n sortBy: z.enum([\"lastSeenAt\", \"createdAt\", \"value\"]).optional(),\n sortOrder: z.enum([\"asc\", \"desc\"]).optional(),\n limit: z.number().int().min(1).max(100).optional().default(50),\n offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const listAssetsTool = {\n name: \"list_assets\",\n title: \"List assets\",\n description:\n \"List attack-surface assets for the organisation. Supports filtering by type, source, status, and free-text search.\",\n annotations: readAnnotations,\n inputSchema: listAssetsInput,\n handler:\n (client: OdinClient) => async (params: z.infer<typeof listAssetsInput>) => {\n const data = await client.get<AssetsListResponse>(\"/api/assets\", {\n ...params,\n stats: params.stats === undefined ? undefined : String(params.stats),\n });\n return {\n markdown: formatAssetsList(data),\n structured: {\n assets: data.assets,\n stats: data.stats,\n pagination: data.pagination,\n },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatPentestsList } from \"../formatters/markdown.js\";\nimport type { PentestListResponse } from \"../types.js\";\nimport { readAnnotations } from \"./finding-tool.js\";\n\nexport const listPentestsInput = z.object({});\n\nexport const listPentestsTool = {\n name: \"list_pentests\",\n title: \"List pentests\",\n description:\n \"List all pentests for the organisation, including status, scheduled window, and in-scope targets.\",\n annotations: readAnnotations,\n inputSchema: listPentestsInput,\n handler: (client: OdinClient) => async () => {\n const data = await client.get<PentestListResponse>(\"/api/pentests\");\n return {\n markdown: formatPentestsList(data),\n structured: { pentests: data.pentests },\n };\n },\n};\n","import { z } from \"zod\";\nimport type { OdinClient } from \"../client.js\";\nimport { formatFindingStatusUpdate } from \"../formatters/markdown.js\";\nimport type { FindingStatusUpdateResponse } from \"../types.js\";\nimport {\n findingReferenceFields,\n resolveFindingId,\n} from \"./finding-reference.js\";\nimport { writeAnnotations } from \"./finding-tool.js\";\n\nexport const updateFindingStatusInput = z\n .object({\n ...findingReferenceFields,\n status: z\n .enum([\"MITIGATING\", \"OPEN_FOR_RETEST\", \"ACKNOWLEDGED\"])\n .describe(\n \"Target status. MITIGATING (shown in Odin as Fixing) means work has started, OPEN_FOR_RETEST means the fix is ready for verification, and ACKNOWLEDGED (shown as Risk accepted) means the organisation accepts the risk for now.\",\n ),\n })\n .refine((value) => Boolean(value.identifier || value.findingId), {\n message: \"Provide either identifier or findingId.\",\n });\n\nexport const updateFindingStatusTool = {\n name: \"update_finding_status\",\n title: \"Update finding status\",\n description:\n \"Change a finding's status by human identifier such as ODI-12 or internal findingId. Requires an API key with Read & Write permission; cookie-authenticated callers also need member role. Only use this after the user explicitly asks for the status change; a matching pull request is not permission to mutate a finding.\",\n annotations: { ...writeAnnotations, idempotentHint: true },\n inputSchema: updateFindingStatusInput,\n handler:\n (client: OdinClient) =>\n async (params: z.infer<typeof updateFindingStatusInput>) => {\n const findingId = await resolveFindingId(client, params);\n const data = await client.patch<FindingStatusUpdateResponse>(\n `/api/findings/${encodeURIComponent(findingId)}/status`,\n { status: params.status },\n );\n return {\n markdown: formatFindingStatusUpdate(data.finding),\n structured: { finding: data.finding },\n };\n },\n};\n","import type { z } from \"zod\";\nimport type { OdinClient } from \"./client.js\";\nimport { additionalFindingTools } from \"./tools/additional-tools.js\";\nimport { getAssetTool } from \"./tools/get-asset.js\";\nimport { getFindingTool } from \"./tools/get-finding.js\";\nimport { getFindingRetestsTool } from \"./tools/get-finding-retests.js\";\nimport { getPentestTool } from \"./tools/get-pentest.js\";\nimport { linkFindingPullRequestTool } from \"./tools/link-finding-pull-request.js\";\nimport { listAssetsTool } from \"./tools/list-assets.js\";\nimport { listFindingsTool } from \"./tools/list-findings.js\";\nimport { listPentestsTool } from \"./tools/list-pentests.js\";\nimport { updateFindingStatusTool } from \"./tools/update-finding-status.js\";\n\nexport interface OdinToolAnnotations {\n readOnlyHint: boolean;\n destructiveHint: boolean;\n idempotentHint: boolean;\n openWorldHint: boolean;\n}\n\nexport interface OdinToolResult {\n markdown: string;\n structured: unknown;\n}\n\n/** One Odin tool, independent of the MCP transport that serves it. */\nexport interface OdinTool {\n name: string;\n title: string;\n description: string;\n inputSchema: z.ZodType;\n annotations: OdinToolAnnotations;\n run: (client: OdinClient, params: unknown) => Promise<OdinToolResult>;\n}\n\nconst definitions = [\n listFindingsTool,\n getFindingTool,\n updateFindingStatusTool,\n linkFindingPullRequestTool,\n getFindingRetestsTool,\n listPentestsTool,\n getPentestTool,\n listAssetsTool,\n getAssetTool,\n ...additionalFindingTools,\n];\n\n/**\n * Every tool odin-mcp serves, in registration order. The MCP server and\n * web-odin's Slack agent both run these, each with its own `OdinClient`.\n */\nexport const ODIN_TOOLS: readonly OdinTool[] = definitions.map((tool) => ({\n name: tool.name,\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema,\n annotations: tool.annotations,\n run: (client, params) =>\n (tool.handler(client) as (params: unknown) => Promise<OdinToolResult>)(\n params,\n ),\n}));\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z","z"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildFixFindingPrompt,
|
|
4
|
+
buildServerInstructions,
|
|
5
|
+
buildSetupPrompt
|
|
6
|
+
} from "./chunk-4LCKW236.js";
|
|
7
|
+
import {
|
|
8
|
+
ODIN_TOOLS
|
|
9
|
+
} from "./chunk-D5FYYTYV.js";
|
|
10
|
+
import {
|
|
11
|
+
OdinApiError
|
|
12
|
+
} from "./chunk-XHJYVUHG.js";
|
|
13
|
+
|
|
14
|
+
// src/server.ts
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
// package.json
|
|
19
|
+
var version = "0.3.0";
|
|
20
|
+
|
|
21
|
+
// src/server.ts
|
|
22
|
+
var PACKAGE_VERSION = version;
|
|
23
|
+
function createOdinMcpServer(client, options = {}) {
|
|
24
|
+
const { appUrl } = options;
|
|
25
|
+
const server = new McpServer(
|
|
26
|
+
{ name: "@borgresearch/odin-mcp", version: PACKAGE_VERSION },
|
|
27
|
+
{ instructions: buildServerInstructions({ appUrl }) }
|
|
28
|
+
);
|
|
29
|
+
server.registerPrompt(
|
|
30
|
+
"setup",
|
|
31
|
+
{
|
|
32
|
+
title: "Set up Odin",
|
|
33
|
+
description: "Learn what Odin does, check what is set up, get recommendations for this repository and reach a first result."
|
|
34
|
+
},
|
|
35
|
+
() => ({
|
|
36
|
+
messages: [
|
|
37
|
+
{
|
|
38
|
+
role: "user",
|
|
39
|
+
content: {
|
|
40
|
+
type: "text",
|
|
41
|
+
text: buildSetupPrompt({ appUrl })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
server.registerPrompt(
|
|
48
|
+
"fix_finding",
|
|
49
|
+
{
|
|
50
|
+
title: "Fix an Odin finding",
|
|
51
|
+
description: "Fix one finding in this repository, open a pull request and link it so Odin can retest the fix.",
|
|
52
|
+
argsSchema: {
|
|
53
|
+
finding: z.string().max(100).optional().describe(
|
|
54
|
+
"Finding ID such as ODI-12. Leave empty to pick the most severe open finding."
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
({ finding }) => ({
|
|
59
|
+
messages: [
|
|
60
|
+
{
|
|
61
|
+
role: "user",
|
|
62
|
+
content: {
|
|
63
|
+
type: "text",
|
|
64
|
+
text: buildFixFindingPrompt({ appUrl, finding })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
for (const tool of ODIN_TOOLS) {
|
|
71
|
+
server.registerTool(
|
|
72
|
+
tool.name,
|
|
73
|
+
{
|
|
74
|
+
title: tool.title,
|
|
75
|
+
description: tool.description,
|
|
76
|
+
inputSchema: tool.inputSchema,
|
|
77
|
+
annotations: tool.annotations
|
|
78
|
+
},
|
|
79
|
+
async (params) => {
|
|
80
|
+
try {
|
|
81
|
+
const { markdown, structured } = await tool.run(client, params);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: markdown }],
|
|
84
|
+
structuredContent: { markdown, ...toObject(structured) }
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
isError: true,
|
|
89
|
+
content: [{ type: "text", text: toErrorMessage(error) }]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return server;
|
|
96
|
+
}
|
|
97
|
+
function toObject(value) {
|
|
98
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
return { data: value };
|
|
102
|
+
}
|
|
103
|
+
function toErrorMessage(error) {
|
|
104
|
+
if (error instanceof OdinApiError) return error.message;
|
|
105
|
+
if (error instanceof Error) return `Unexpected error: ${error.message}`;
|
|
106
|
+
return `Unexpected error: ${String(error)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
PACKAGE_VERSION,
|
|
111
|
+
createOdinMcpServer
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=chunk-ODZB3YP2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../package.json"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { version } from \"../package.json\";\nimport { OdinApiError, type OdinClient } from \"./client.js\";\nimport {\n buildFixFindingPrompt,\n buildServerInstructions,\n buildSetupPrompt,\n} from \"./guidance.js\";\nimport { ODIN_TOOLS } from \"./tools.js\";\n\nexport const PACKAGE_VERSION = version;\n\nexport interface OdinMcpServerOptions {\n /** Public Odin origin for links in guidance. Defaults to production. */\n appUrl?: string;\n}\n\nexport function createOdinMcpServer(\n client: OdinClient,\n options: OdinMcpServerOptions = {},\n) {\n const { appUrl } = options;\n const server = new McpServer(\n { name: \"@borgresearch/odin-mcp\", version: PACKAGE_VERSION },\n { instructions: buildServerInstructions({ appUrl }) },\n );\n\n server.registerPrompt(\n \"setup\",\n {\n title: \"Set up Odin\",\n description:\n \"Learn what Odin does, check what is set up, get recommendations for this repository and reach a first result.\",\n },\n () => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: buildSetupPrompt({ appUrl }),\n },\n },\n ],\n }),\n );\n\n server.registerPrompt(\n \"fix_finding\",\n {\n title: \"Fix an Odin finding\",\n description:\n \"Fix one finding in this repository, open a pull request and link it so Odin can retest the fix.\",\n argsSchema: {\n finding: z\n .string()\n .max(100)\n .optional()\n .describe(\n \"Finding ID such as ODI-12. Leave empty to pick the most severe open finding.\",\n ),\n },\n },\n ({ finding }) => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: buildFixFindingPrompt({ appUrl, finding }),\n },\n },\n ],\n }),\n );\n\n for (const tool of ODIN_TOOLS) {\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema,\n annotations: tool.annotations,\n },\n async (params: unknown) => {\n try {\n const { markdown, structured } = await tool.run(client, params);\n return {\n content: [{ type: \"text\" as const, text: markdown }],\n structuredContent: { markdown, ...toObject(structured) },\n };\n } catch (error) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: toErrorMessage(error) }],\n };\n }\n },\n );\n }\n\n return server;\n}\n\nfunction toObject(value: unknown): Record<string, unknown> {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return { data: value };\n}\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof OdinApiError) return error.message;\n if (error instanceof Error) return `Unexpected error: ${error.message}`;\n return `Unexpected error: ${String(error)}`;\n}\n","{\n \"name\": \"@borgresearch/odin-mcp\",\n \"version\": \"0.3.0\",\n \"mcpName\": \"io.github.BorgDevelopment/odin-mcp\",\n \"description\": \"MCP server for the Odin security API\",\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"odin\",\n \"security\"\n ],\n \"homepage\": \"https://github.com/BorgDevelopment/borg-platform/tree/develop/frontend/packages/odin-mcp\",\n \"bugs\": {\n \"url\": \"https://github.com/BorgDevelopment/borg-platform/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/BorgDevelopment/borg-platform.git\",\n \"directory\": \"frontend/packages/odin-mcp\"\n },\n \"license\": \"MIT\",\n \"author\": \"Borg Research\",\n \"type\": \"module\",\n \"bin\": {\n \"odin-mcp\": \"./dist/index.js\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./server\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\"\n },\n \"./http\": {\n \"types\": \"./dist/http.d.ts\",\n \"import\": \"./dist/http.js\"\n },\n \"./client\": {\n \"types\": \"./dist/client.d.ts\",\n \"import\": \"./dist/client.js\"\n },\n \"./guidance\": {\n \"types\": \"./dist/guidance.d.ts\",\n \"import\": \"./dist/guidance.js\"\n },\n \"./tools\": {\n \"types\": \"./dist/tools.d.ts\",\n \"import\": \"./dist/tools.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"publishConfig\": {\n \"access\": \"public\",\n \"registry\": \"https://registry.npmjs.org/\"\n },\n \"engines\": {\n \"node\": \">=24.0.0\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:run\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"format\": \"biome format --write .\",\n \"inspect\": \"mcp-inspector node dist/index.js\",\n \"inspect:dev\": \"pnpm build && pnpm inspect\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.31.0\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.3.14\",\n \"@modelcontextprotocol/inspector\": \"^0.21.2\",\n \"@types/node\": \"^24.10.1\",\n \"tsup\": \"^8.3.5\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.1.11\"\n },\n \"packageManager\": \"pnpm@11.2.2\"\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,iBAAiB;AAC1B,SAAS,SAAS;;;ACChB,cAAW;;;ADSN,IAAM,kBAAkB;AAOxB,SAAS,oBACd,QACA,UAAgC,CAAC,GACjC;AACA,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,IAAI;AAAA,IACjB,EAAE,MAAM,0BAA0B,SAAS,gBAAgB;AAAA,IAC3D,EAAE,cAAc,wBAAwB,EAAE,OAAO,CAAC,EAAE;AAAA,EACtD;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,MACL,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,YACP,MAAM;AAAA,YACN,MAAM,iBAAiB,EAAE,OAAO,CAAC;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aACE;AAAA,MACF,YAAY;AAAA,QACV,SAAS,EACN,OAAO,EACP,IAAI,GAAG,EACP,SAAS,EACT;AAAA,UACC;AAAA,QACF;AAAA,MACJ;AAAA,IACF;AAAA,IACA,CAAC,EAAE,QAAQ,OAAO;AAAA,MAChB,UAAU;AAAA,QACR;AAAA,UACE,MAAM;AAAA,UACN,SAAS;AAAA,YACP,MAAM;AAAA,YACN,MAAM,sBAAsB,EAAE,QAAQ,QAAQ,CAAC;AAAA,UACjD;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,QAAQ,YAAY;AAC7B,WAAO;AAAA,MACL,KAAK;AAAA,MACL;AAAA,QACE,OAAO,KAAK;AAAA,QACZ,aAAa,KAAK;AAAA,QAClB,aAAa,KAAK;AAAA,QAClB,aAAa,KAAK;AAAA,MACpB;AAAA,MACA,OAAO,WAAoB;AACzB,YAAI;AACF,gBAAM,EAAE,UAAU,WAAW,IAAI,MAAM,KAAK,IAAI,QAAQ,MAAM;AAC9D,iBAAO;AAAA,YACL,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,SAAS,CAAC;AAAA,YACnD,mBAAmB,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;AAAA,UACzD;AAAA,QACF,SAAS,OAAO;AACd,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,eAAe,KAAK,EAAE,CAAC;AAAA,UAClE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAyC;AACzD,MAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,MAAM;AACvB;AAEA,SAAS,eAAe,OAAwB;AAC9C,MAAI,iBAAiB,aAAc,QAAO,MAAM;AAChD,MAAI,iBAAiB,MAAO,QAAO,qBAAqB,MAAM,OAAO;AACrE,SAAO,qBAAqB,OAAO,KAAK,CAAC;AAC3C;","names":[]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/client.ts
|
|
4
|
+
var OdinApiError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
path;
|
|
7
|
+
method;
|
|
8
|
+
retryAfterSeconds;
|
|
9
|
+
body;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
super(params.message);
|
|
12
|
+
this.name = "OdinApiError";
|
|
13
|
+
this.status = params.status;
|
|
14
|
+
this.path = params.path;
|
|
15
|
+
this.method = params.method;
|
|
16
|
+
this.retryAfterSeconds = params.retryAfterSeconds;
|
|
17
|
+
this.body = params.body;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var DEFAULT_ODIN_BASE_URL = "https://odin.borghq.io";
|
|
21
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]);
|
|
22
|
+
var OdinClient = class {
|
|
23
|
+
baseUrl;
|
|
24
|
+
apiKey;
|
|
25
|
+
fetchImpl;
|
|
26
|
+
vercelProtectionBypassSecret;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
if (!options.apiKey) {
|
|
29
|
+
throw new Error("OdinClient requires an apiKey");
|
|
30
|
+
}
|
|
31
|
+
this.apiKey = options.apiKey;
|
|
32
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(
|
|
33
|
+
/\/+$/,
|
|
34
|
+
""
|
|
35
|
+
);
|
|
36
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
37
|
+
this.vercelProtectionBypassSecret = options.vercelProtectionBypassSecret?.trim() || void 0;
|
|
38
|
+
}
|
|
39
|
+
async get(path, searchParams) {
|
|
40
|
+
const url = this.buildUrl(path, searchParams);
|
|
41
|
+
return this.request("GET", url, path);
|
|
42
|
+
}
|
|
43
|
+
async patch(path, body) {
|
|
44
|
+
const url = this.buildUrl(path);
|
|
45
|
+
return this.request("PATCH", url, path, body);
|
|
46
|
+
}
|
|
47
|
+
async put(path, body) {
|
|
48
|
+
const url = this.buildUrl(path);
|
|
49
|
+
return this.request("PUT", url, path, body);
|
|
50
|
+
}
|
|
51
|
+
// Public HTTP method API — kept alongside get/patch even when unused internally
|
|
52
|
+
// fallow-ignore-next-line unused-class-member
|
|
53
|
+
async post(path, body) {
|
|
54
|
+
const url = this.buildUrl(path);
|
|
55
|
+
return this.request("POST", url, path, body);
|
|
56
|
+
}
|
|
57
|
+
async delete(path, body) {
|
|
58
|
+
return this.request("DELETE", this.buildUrl(path), path, body);
|
|
59
|
+
}
|
|
60
|
+
async getRedirectUrl(path, searchParams) {
|
|
61
|
+
return this.request(
|
|
62
|
+
"GET",
|
|
63
|
+
this.buildUrl(path, searchParams),
|
|
64
|
+
path,
|
|
65
|
+
void 0,
|
|
66
|
+
true
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
buildUrl(path, searchParams) {
|
|
70
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, this.baseUrl);
|
|
71
|
+
if (url.origin !== new URL(this.baseUrl).origin) {
|
|
72
|
+
throw new Error("Odin API paths must stay on the configured origin.");
|
|
73
|
+
}
|
|
74
|
+
if (searchParams) {
|
|
75
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
76
|
+
if (value === void 0 || value === "") continue;
|
|
77
|
+
url.searchParams.set(key, String(value));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return url.toString();
|
|
81
|
+
}
|
|
82
|
+
async request(method, url, path, body, redirectUrl = false) {
|
|
83
|
+
const headers = {
|
|
84
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Accept: "application/json"
|
|
87
|
+
};
|
|
88
|
+
if (this.vercelProtectionBypassSecret) {
|
|
89
|
+
headers["x-vercel-protection-bypass"] = this.vercelProtectionBypassSecret;
|
|
90
|
+
}
|
|
91
|
+
const response = await this.fetchImpl(url, {
|
|
92
|
+
method,
|
|
93
|
+
headers,
|
|
94
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
95
|
+
redirect: "manual"
|
|
96
|
+
});
|
|
97
|
+
if (redirectUrl && [301, 302, 303, 307, 308].includes(response.status)) {
|
|
98
|
+
const location = response.headers.get("Location");
|
|
99
|
+
if (!location) throw new Error("Odin returned a redirect without a URL.");
|
|
100
|
+
const target = new URL(location, url);
|
|
101
|
+
if (target.protocol !== "https:" || target.username || target.password) {
|
|
102
|
+
throw new Error("Odin returned an unsafe redirect URL.");
|
|
103
|
+
}
|
|
104
|
+
return target.toString();
|
|
105
|
+
}
|
|
106
|
+
const rawText = await response.text();
|
|
107
|
+
const parsed = rawText ? safeJsonParse(rawText) : null;
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const retryAfterSeconds = parseRetryAfter(
|
|
110
|
+
response.headers.get("Retry-After")
|
|
111
|
+
);
|
|
112
|
+
throw new OdinApiError({
|
|
113
|
+
status: response.status,
|
|
114
|
+
path,
|
|
115
|
+
method,
|
|
116
|
+
retryAfterSeconds,
|
|
117
|
+
body: parsed ?? rawText,
|
|
118
|
+
message: formatErrorMessage({
|
|
119
|
+
status: response.status,
|
|
120
|
+
method,
|
|
121
|
+
path,
|
|
122
|
+
retryAfterSeconds,
|
|
123
|
+
parsed,
|
|
124
|
+
rawText
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (redirectUrl) throw new Error("Odin did not return a redirect URL.");
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
function safeJsonParse(text) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(text);
|
|
135
|
+
} catch {
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function parseRetryAfter(header) {
|
|
140
|
+
if (!header) return null;
|
|
141
|
+
const trimmed = header.trim();
|
|
142
|
+
if (/^\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);
|
|
143
|
+
const dateMs = Date.parse(trimmed);
|
|
144
|
+
if (Number.isNaN(dateMs)) return null;
|
|
145
|
+
const seconds = Math.ceil((dateMs - Date.now()) / 1e3);
|
|
146
|
+
return seconds > 0 ? seconds : null;
|
|
147
|
+
}
|
|
148
|
+
function formatErrorMessage(input) {
|
|
149
|
+
const serverMessage = extractServerMessage(input.parsed);
|
|
150
|
+
const actionable = actionableMessage(input);
|
|
151
|
+
if (actionable) {
|
|
152
|
+
return serverMessage ? `${actionable} (server: ${serverMessage})` : actionable;
|
|
153
|
+
}
|
|
154
|
+
const suffix = serverMessage ? ` \u2014 ${serverMessage}` : input.rawText ? ` \u2014 ${input.rawText.slice(0, 200)}` : "";
|
|
155
|
+
return `Odin API ${input.status} on ${input.path}${suffix}.`;
|
|
156
|
+
}
|
|
157
|
+
function actionableMessage(input) {
|
|
158
|
+
switch (input.status) {
|
|
159
|
+
case 401:
|
|
160
|
+
return "ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.";
|
|
161
|
+
case 403:
|
|
162
|
+
if (WRITE_METHODS.has(input.method)) {
|
|
163
|
+
return "API key does not have write permission. Create a new key with 'Read & Write' permissions.";
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
case 404:
|
|
167
|
+
if (isFindingDetailPath(input.path)) {
|
|
168
|
+
return "Finding not found. Check the finding ID is correct.";
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
case 429: {
|
|
172
|
+
const n = input.retryAfterSeconds;
|
|
173
|
+
return n ? `Rate limit exceeded. Try again in ${n} seconds.` : "Rate limit exceeded. Try again shortly.";
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function isFindingDetailPath(path) {
|
|
180
|
+
return /^\/api\/findings\/[^/]+(\/.*)?$/.test(path);
|
|
181
|
+
}
|
|
182
|
+
function extractServerMessage(parsed) {
|
|
183
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
184
|
+
const err = parsed.error;
|
|
185
|
+
if (typeof err === "string") return err;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
OdinApiError,
|
|
192
|
+
DEFAULT_ODIN_BASE_URL,
|
|
193
|
+
OdinClient
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=chunk-XHJYVUHG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["export class OdinApiError extends Error {\n readonly status: number;\n readonly path: string;\n readonly method: string;\n readonly retryAfterSeconds: number | null;\n readonly body: unknown;\n\n constructor(params: {\n status: number;\n path: string;\n method: string;\n retryAfterSeconds: number | null;\n body: unknown;\n message: string;\n }) {\n super(params.message);\n this.name = \"OdinApiError\";\n this.status = params.status;\n this.path = params.path;\n this.method = params.method;\n this.retryAfterSeconds = params.retryAfterSeconds;\n this.body = params.body;\n }\n}\n\nexport interface OdinClientOptions {\n apiKey: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n vercelProtectionBypassSecret?: string;\n}\n\nexport const DEFAULT_ODIN_BASE_URL = \"https://odin.borghq.io\";\n\nconst WRITE_METHODS = new Set([\"PATCH\", \"POST\", \"PUT\", \"DELETE\"]);\n\nexport class OdinClient {\n private readonly baseUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n private readonly vercelProtectionBypassSecret: string | undefined;\n\n constructor(options: OdinClientOptions) {\n if (!options.apiKey) {\n throw new Error(\"OdinClient requires an apiKey\");\n }\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_ODIN_BASE_URL).replace(\n /\\/+$/,\n \"\",\n );\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.vercelProtectionBypassSecret =\n options.vercelProtectionBypassSecret?.trim() || undefined;\n }\n\n async get<T>(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): Promise<T> {\n const url = this.buildUrl(path, searchParams);\n return this.request<T>(\"GET\", url, path);\n }\n\n async patch<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"PATCH\", url, path, body);\n }\n\n async put<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"PUT\", url, path, body);\n }\n\n // Public HTTP method API — kept alongside get/patch even when unused internally\n // fallow-ignore-next-line unused-class-member\n async post<T>(path: string, body: unknown): Promise<T> {\n const url = this.buildUrl(path);\n return this.request<T>(\"POST\", url, path, body);\n }\n\n async delete<T>(path: string, body?: unknown): Promise<T> {\n return this.request<T>(\"DELETE\", this.buildUrl(path), path, body);\n }\n\n async getRedirectUrl(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): Promise<string> {\n return this.request<string>(\n \"GET\",\n this.buildUrl(path, searchParams),\n path,\n undefined,\n true,\n );\n }\n\n private buildUrl(\n path: string,\n searchParams?: Record<string, string | number | undefined>,\n ): string {\n const url = new URL(path.startsWith(\"/\") ? path : `/${path}`, this.baseUrl);\n if (url.origin !== new URL(this.baseUrl).origin) {\n throw new Error(\"Odin API paths must stay on the configured origin.\");\n }\n if (searchParams) {\n for (const [key, value] of Object.entries(searchParams)) {\n if (value === undefined || value === \"\") continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private async request<T>(\n method: string,\n url: string,\n path: string,\n body?: unknown,\n redirectUrl = false,\n ): Promise<T> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n };\n if (this.vercelProtectionBypassSecret) {\n headers[\"x-vercel-protection-bypass\"] = this.vercelProtectionBypassSecret;\n }\n\n const response = await this.fetchImpl(url, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n redirect: \"manual\",\n });\n\n if (redirectUrl && [301, 302, 303, 307, 308].includes(response.status)) {\n const location = response.headers.get(\"Location\");\n if (!location) throw new Error(\"Odin returned a redirect without a URL.\");\n const target = new URL(location, url);\n if (target.protocol !== \"https:\" || target.username || target.password) {\n throw new Error(\"Odin returned an unsafe redirect URL.\");\n }\n return target.toString() as T;\n }\n\n const rawText = await response.text();\n const parsed = rawText ? safeJsonParse(rawText) : null;\n\n if (!response.ok) {\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get(\"Retry-After\"),\n );\n throw new OdinApiError({\n status: response.status,\n path,\n method,\n retryAfterSeconds,\n body: parsed ?? rawText,\n message: formatErrorMessage({\n status: response.status,\n method,\n path,\n retryAfterSeconds,\n parsed,\n rawText,\n }),\n });\n }\n\n if (redirectUrl) throw new Error(\"Odin did not return a redirect URL.\");\n\n return parsed as T;\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction parseRetryAfter(header: string | null): number | null {\n if (!header) return null;\n const trimmed = header.trim();\n if (/^\\d+$/.test(trimmed)) return Number.parseInt(trimmed, 10);\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const seconds = Math.ceil((dateMs - Date.now()) / 1000);\n return seconds > 0 ? seconds : null;\n}\n\ninterface FormatErrorInput {\n status: number;\n method: string;\n path: string;\n retryAfterSeconds: number | null;\n parsed: unknown;\n rawText: string;\n}\n\nfunction formatErrorMessage(input: FormatErrorInput): string {\n const serverMessage = extractServerMessage(input.parsed);\n const actionable = actionableMessage(input);\n if (actionable) {\n return serverMessage\n ? `${actionable} (server: ${serverMessage})`\n : actionable;\n }\n const suffix = serverMessage\n ? ` — ${serverMessage}`\n : input.rawText\n ? ` — ${input.rawText.slice(0, 200)}`\n : \"\";\n return `Odin API ${input.status} on ${input.path}${suffix}.`;\n}\n\nfunction actionableMessage(input: FormatErrorInput): string | null {\n switch (input.status) {\n case 401:\n return \"ODIN_API_KEY is not set or invalid. Create one at Management > API Keys in the Odin dashboard.\";\n case 403:\n if (WRITE_METHODS.has(input.method)) {\n return \"API key does not have write permission. Create a new key with 'Read & Write' permissions.\";\n }\n return null;\n case 404:\n if (isFindingDetailPath(input.path)) {\n return \"Finding not found. Check the finding ID is correct.\";\n }\n return null;\n case 429: {\n const n = input.retryAfterSeconds;\n return n\n ? `Rate limit exceeded. Try again in ${n} seconds.`\n : \"Rate limit exceeded. Try again shortly.\";\n }\n default:\n return null;\n }\n}\n\nfunction isFindingDetailPath(path: string): boolean {\n // /api/findings/{id} or /api/findings/{id}/status\n return /^\\/api\\/findings\\/[^/]+(\\/.*)?$/.test(path);\n}\n\nfunction extractServerMessage(parsed: unknown): string | null {\n if (parsed && typeof parsed === \"object\" && \"error\" in parsed) {\n const err = (parsed as { error: unknown }).error;\n if (typeof err === \"string\") return err;\n }\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
declare class OdinApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly method: string;
|
|
5
|
+
readonly retryAfterSeconds: number | null;
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
constructor(params: {
|
|
8
|
+
status: number;
|
|
9
|
+
path: string;
|
|
10
|
+
method: string;
|
|
11
|
+
retryAfterSeconds: number | null;
|
|
12
|
+
body: unknown;
|
|
13
|
+
message: string;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
interface OdinClientOptions {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
vercelProtectionBypassSecret?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const DEFAULT_ODIN_BASE_URL = "https://odin.borghq.io";
|
|
23
|
+
declare class OdinClient {
|
|
24
|
+
private readonly baseUrl;
|
|
25
|
+
private readonly apiKey;
|
|
26
|
+
private readonly fetchImpl;
|
|
27
|
+
private readonly vercelProtectionBypassSecret;
|
|
28
|
+
constructor(options: OdinClientOptions);
|
|
29
|
+
get<T>(path: string, searchParams?: Record<string, string | number | undefined>): Promise<T>;
|
|
30
|
+
patch<T>(path: string, body: unknown): Promise<T>;
|
|
31
|
+
put<T>(path: string, body: unknown): Promise<T>;
|
|
32
|
+
post<T>(path: string, body: unknown): Promise<T>;
|
|
33
|
+
delete<T>(path: string, body?: unknown): Promise<T>;
|
|
34
|
+
getRedirectUrl(path: string, searchParams?: Record<string, string | number | undefined>): Promise<string>;
|
|
35
|
+
private buildUrl;
|
|
36
|
+
private request;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { DEFAULT_ODIN_BASE_URL, OdinApiError, OdinClient, type OdinClientOptions };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|