| export interface DomainStatus { | ||
| domain: { | ||
| hostname: string; | ||
| status: string; | ||
| sslStatus: string | null; | ||
| } | null; | ||
| cnameTarget: string; | ||
| } | ||
| export declare class DomainError extends Error { | ||
| code: string; | ||
| constructor(code: string, message?: string); | ||
| } | ||
| /** The endpoint's custom domain, if any (polls certificate issuance). */ | ||
| export declare function getDomain(subdomain: string): Promise<DomainStatus>; | ||
| /** Attach a hostname (one per endpoint). Returns pending until the CNAME lands. */ | ||
| export declare function attachDomain(subdomain: string, hostname: string): Promise<DomainStatus>; | ||
| /** Detach the endpoint's custom domain. */ | ||
| export declare function removeDomain(subdomain: string): Promise<{ | ||
| removed: boolean; | ||
| }>; |
| /** | ||
| * Custom domains for webhook endpoints, over the token-authed /api/me | ||
| * surface - the same machinery as the console's domain card: attach a | ||
| * hostname you own, add the CNAME, and the certificate issues on its own. | ||
| */ | ||
| import { getToken } from './credentials.js'; | ||
| const API_SERVER = process.env.OTTERKIT_API_URL || 'https://api.otterkit.com'; | ||
| export class DomainError extends Error { | ||
| code; | ||
| constructor(code, message) { | ||
| super(message ?? code); | ||
| this.code = code; | ||
| this.name = 'DomainError'; | ||
| } | ||
| } | ||
| async function domainFetch(subdomain, init) { | ||
| const token = getToken(); | ||
| if (!token) | ||
| throw new DomainError('not_logged_in', 'Run `otterkit login` first.'); | ||
| const resp = await fetch(`${API_SERVER}/api/me/webhooks/${encodeURIComponent(subdomain)}/domain`, { | ||
| ...init, | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| ...(init.body ? { 'Content-Type': 'application/json' } : {}), | ||
| }, | ||
| }); | ||
| const json = (await resp.json().catch(() => ({}))); | ||
| if (resp.status === 404) { | ||
| throw new DomainError('not_found', `No webhook endpoint "${subdomain}" on this account.`); | ||
| } | ||
| if (!resp.ok || !json.data) { | ||
| throw new DomainError(json.error ?? `http_${resp.status}`, json.error); | ||
| } | ||
| return json.data; | ||
| } | ||
| /** The endpoint's custom domain, if any (polls certificate issuance). */ | ||
| export async function getDomain(subdomain) { | ||
| return (await domainFetch(subdomain, { method: 'GET' })); | ||
| } | ||
| /** Attach a hostname (one per endpoint). Returns pending until the CNAME lands. */ | ||
| export async function attachDomain(subdomain, hostname) { | ||
| return (await domainFetch(subdomain, { | ||
| method: 'POST', | ||
| body: JSON.stringify({ hostname }), | ||
| })); | ||
| } | ||
| /** Detach the endpoint's custom domain. */ | ||
| export async function removeDomain(subdomain) { | ||
| return (await domainFetch(subdomain, { method: 'DELETE' })); | ||
| } |
+15
-0
@@ -38,2 +38,17 @@ export interface StoredRequest { | ||
| }>; | ||
| /** | ||
| * Re-deliver a server-stored capture to a target URL, with optional | ||
| * method/headers/body overrides. Cloud endpoints only (they hold the | ||
| * history); local-log captures replay with `otterkit replay`. | ||
| */ | ||
| export declare function replayStored(subdomain: string, requestId: string, opts: { | ||
| targetUrl: string; | ||
| method?: string; | ||
| headers?: Record<string, string>; | ||
| bodyText?: string; | ||
| }): Promise<{ | ||
| delivered: boolean; | ||
| status: number; | ||
| durationMs: number; | ||
| }>; | ||
| export interface SchemaInferResult { | ||
@@ -40,0 +55,0 @@ subdomain: string; |
+20
-0
@@ -52,2 +52,22 @@ /** | ||
| /** | ||
| * Re-deliver a server-stored capture to a target URL, with optional | ||
| * method/headers/body overrides. Cloud endpoints only (they hold the | ||
| * history); local-log captures replay with `otterkit replay`. | ||
| */ | ||
| export async function replayStored(subdomain, requestId, opts) { | ||
| const token = getToken(); | ||
| if (!token) | ||
| throw new HistoryError('not_logged_in', 'Run `otterkit login` first.'); | ||
| const resp = await fetch(`${API_SERVER}/api/me/webhooks/${encodeURIComponent(subdomain)}/requests/${encodeURIComponent(requestId)}/replay`, { | ||
| method: 'POST', | ||
| headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(opts), | ||
| }); | ||
| const json = (await resp.json().catch(() => ({}))); | ||
| if (!resp.ok || !json.data) { | ||
| throw new HistoryError(json.error ?? `replay_failed_${resp.status}`, json.message); | ||
| } | ||
| return json.data; | ||
| } | ||
| /** | ||
| * Infer JSON Schema + TypeScript types from a session's stored request | ||
@@ -54,0 +74,0 @@ * bodies. Same token-authed surface and error mapping as fetchHistory; the |
+84
-2
@@ -26,6 +26,7 @@ /** | ||
| import { sendCore, verifyCore, SendError } from './send.js'; | ||
| import { createPulse } from './pulse.js'; | ||
| import { createPulse, getPulseConfig, updatePulse } from './pulse.js'; | ||
| import { attachDomain, getDomain, removeDomain } from './domains.js'; | ||
| import { listEvents } from './providers.js'; | ||
| import { createWait, listWaits, cancelWait, sessionToken } from './waits.js'; | ||
| import { fetchHistory, fetchSchema } from './history.js'; | ||
| import { fetchHistory, fetchSchema, replayStored } from './history.js'; | ||
| const { version } = createRequire(import.meta.url)('../package.json'); | ||
@@ -504,2 +505,83 @@ const API_SERVER = process.env.OTTERKIT_API_URL || 'https://api.otterkit.com'; | ||
| }); | ||
| server.registerTool('pulse_update', { | ||
| title: 'Edit a live pulse in place', | ||
| description: "Change an owned pulse's schedule without recreating it: interval/window, mode, and for active mode the target URL, method, and body. Run history, the pulse URL, and the billing window all survive; the next run (or check-in window) is measured from the update. Omitted fields keep their current values.", | ||
| inputSchema: { | ||
| subdomain: z.string().describe('The pulse to edit'), | ||
| mode: z.enum(['active', 'expect']).optional().describe('Switch mode (default: keep)'), | ||
| interval: z | ||
| .string() | ||
| .optional() | ||
| .describe('New interval/window e.g. 30s, 10m, 4h, 1d (min 30s; default: keep)'), | ||
| url: z.string().optional().describe('Active mode: new target URL (default: keep)'), | ||
| method: z.string().optional().describe('Active mode: new HTTP method (default: keep)'), | ||
| body: z.string().optional().describe('Active mode: new request body (default: keep)'), | ||
| }, | ||
| }, async (args) => { | ||
| try { | ||
| const current = await getPulseConfig(args.subdomain); | ||
| const mode = args.mode ?? current.mode; | ||
| const r = await updatePulse(args.subdomain, { | ||
| mode, | ||
| interval: args.interval ?? `${Math.round(current.intervalMs / 1000)}s`, | ||
| url: mode === 'active' ? (args.url ?? current.url ?? undefined) : undefined, | ||
| method: mode === 'active' ? (args.method ?? current.method ?? 'GET') : undefined, | ||
| body: mode === 'active' ? (args.body ?? current.bodyText ?? undefined) : undefined, | ||
| }); | ||
| return ok({ updated: true, nextRunAt: r.nextRunAt }); | ||
| } | ||
| catch (e) { | ||
| return mapError(e); | ||
| } | ||
| }); | ||
| server.registerTool('webhook_domain', { | ||
| title: 'Custom domain on a webhook endpoint', | ||
| description: "Serve an owned webhook endpoint on a domain the user controls (e.g. hooks.their-company.com) - the certificate is issued and renewed automatically once they add the returned CNAME record. Actions: status (poll issuance), attach {hostname}, remove. One hostname per endpoint.", | ||
| inputSchema: { | ||
| subdomain: z.string().describe('Webhook endpoint to configure'), | ||
| action: z.enum(['status', 'attach', 'remove']), | ||
| hostname: z.string().optional().describe('attach: the hostname to serve the endpoint on'), | ||
| }, | ||
| }, async (args) => { | ||
| try { | ||
| if (args.action === 'remove') | ||
| return ok(await removeDomain(args.subdomain)); | ||
| if (args.action === 'attach') { | ||
| if (!args.hostname) | ||
| return err('hostname_required', {}); | ||
| return ok(await attachDomain(args.subdomain, args.hostname)); | ||
| } | ||
| return ok(await getDomain(args.subdomain)); | ||
| } | ||
| catch (e) { | ||
| return mapError(e); | ||
| } | ||
| }); | ||
| server.registerTool('history_replay', { | ||
| title: 'Replay a server-stored capture to a URL', | ||
| description: 'Re-deliver a request from server-side stored history (webhook_history returns the ids) to any target URL - original method, headers (transport noise stripped), and body, plus an X-OtterKit-Replay marker. Optional overrides edit the method, headers, or body first. Cloud endpoints only; local-log captures replay with request_replay.', | ||
| inputSchema: { | ||
| subdomain: z.string().describe('Endpoint whose stored history holds the request'), | ||
| requestId: z.string().describe('Stored request id (from webhook_history)'), | ||
| targetUrl: z.string().describe('Where to deliver the copy'), | ||
| method: z.string().optional().describe('Override the HTTP method'), | ||
| headers: z | ||
| .record(z.string(), z.string()) | ||
| .optional() | ||
| .describe('Replace the header set entirely'), | ||
| bodyText: z.string().optional().describe('Replace the body'), | ||
| }, | ||
| }, async (args) => { | ||
| try { | ||
| return ok(await replayStored(args.subdomain, args.requestId, { | ||
| targetUrl: args.targetUrl, | ||
| method: args.method, | ||
| headers: args.headers, | ||
| bodyText: args.bodyText, | ||
| })); | ||
| } | ||
| catch (e) { | ||
| return mapError(e); | ||
| } | ||
| }); | ||
| server.registerTool('webhook_history', { | ||
@@ -506,0 +588,0 @@ title: 'Server-side stored capture history', |
+22
-0
@@ -35,1 +35,23 @@ export interface PulseOptions { | ||
| export declare function stopPulse(subdomain: string, stopToken: string): Promise<void>; | ||
| export interface PulseConfig { | ||
| mode: 'active' | 'expect'; | ||
| intervalMs: number; | ||
| url: string | null; | ||
| method: string | null; | ||
| bodyText: string | null; | ||
| } | ||
| /** The live schedule config of an owned pulse (url/method/body included). */ | ||
| export declare function getPulseConfig(subdomain: string): Promise<PulseConfig>; | ||
| /** | ||
| * Edit a live pulse's schedule in place: run history, the URL, and the | ||
| * billing window all stay; the next run/window is measured from now. | ||
| */ | ||
| export declare function updatePulse(subdomain: string, opts: { | ||
| mode: 'active' | 'expect'; | ||
| interval: string; | ||
| url?: string; | ||
| method?: string; | ||
| body?: string; | ||
| }): Promise<{ | ||
| nextRunAt: string; | ||
| }>; |
+33
-0
@@ -47,1 +47,34 @@ /** | ||
| } | ||
| /* ── edit (token-authed /api/me surface - works from any machine) ────── */ | ||
| const API_SERVER = process.env.OTTERKIT_API_URL || 'https://api.otterkit.com'; | ||
| /** The live schedule config of an owned pulse (url/method/body included). */ | ||
| export async function getPulseConfig(subdomain) { | ||
| const token = getToken(); | ||
| if (!token) | ||
| throw new PulseError('not_logged_in', 'Run `otterkit login` first.'); | ||
| const resp = await fetch(`${API_SERVER}/api/me/pulses/${encodeURIComponent(subdomain)}/config`, { headers: { Authorization: `Bearer ${token}` } }); | ||
| const json = (await resp.json().catch(() => ({}))); | ||
| if (!resp.ok || !json.data) { | ||
| throw new PulseError(json.error ?? `http_${resp.status}`, json.error); | ||
| } | ||
| return json.data.config; | ||
| } | ||
| /** | ||
| * Edit a live pulse's schedule in place: run history, the URL, and the | ||
| * billing window all stay; the next run/window is measured from now. | ||
| */ | ||
| export async function updatePulse(subdomain, opts) { | ||
| const token = getToken(); | ||
| if (!token) | ||
| throw new PulseError('not_logged_in', 'Run `otterkit login` first.'); | ||
| const resp = await fetch(`${API_SERVER}/api/me/pulses/${encodeURIComponent(subdomain)}`, { | ||
| method: 'PATCH', | ||
| headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(opts), | ||
| }); | ||
| const json = (await resp.json().catch(() => ({}))); | ||
| if (!resp.ok || !json.data) { | ||
| throw new PulseError(json.error ?? `http_${resp.status}`, json.error); | ||
| } | ||
| return { nextRunAt: json.data.nextRunAt }; | ||
| } |
+1
-1
| { | ||
| "name": "otterkit", | ||
| "version": "0.37.0", | ||
| "version": "0.39.0", | ||
| "description": "OtterKit CLI - provision and connect tunnels for AI agents", | ||
@@ -5,0 +5,0 @@ "mcpName": "io.github.useotterkit/otterkit", |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
293996
5.38%50
4.17%6837
4.96%30
7.14%27
17.39%