@soba-so/sdk 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
  ```js
6
6
  import { Soba } from "@soba-so/sdk"
7
+ import { sobaUserId } from "@soba-so/sdk/identity"
7
8
 
8
9
  const soba = new Soba({ apiKey: process.env.SOBA_KEY })
9
10
 
10
11
  const run = await soba.run({
11
- user: session.user.id,
12
+ // Stable across database resets — see "Identifying users" below.
13
+ user: sobaUserId(session.user.username),
12
14
  model: "auto",
13
15
  prompt: "Summarise everything I saved this week",
14
16
  tools: [listNotes, readNote], // your code, executed in your app
@@ -20,6 +22,31 @@ for await (const event of run) {
20
22
  }
21
23
  ```
22
24
 
25
+ ## Identifying users
26
+
27
+ `user` is load-bearing: it is how a run is attributed to a person, therefore how
28
+ it is metered, therefore how Soba knows whose machine it may reach. Soba keys an
29
+ end user on `(app, user, environment)` and nothing else.
30
+
31
+ Resolve it **on the server** from the session — a `user` accepted from the
32
+ browser lets anyone run on anyone else's machine — and send the **same** id for
33
+ the run and for the connect session.
34
+
35
+ And pick one that outlives your database. `session.user.id` is usually a random
36
+ key minted at sign-up; it is stable only as long as that row is. Reset or move
37
+ the database and the same person arrives as somebody new, leaving the machine
38
+ they paired attached to an id you will never send again. If your own ids are not
39
+ stable, derive one:
40
+
41
+ ```js
42
+ import { sobaUserId } from "@soba-so/sdk/identity"
43
+
44
+ const user = sobaUserId(session.user.username)
45
+ ```
46
+
47
+ A server-only subpath, so reaching for it in client code fails at your bundler
48
+ rather than in production. Full reasoning: https://soba.so/docs/identity
49
+
23
50
  ## Why this exists alongside `chat/completions`
24
51
 
25
52
  `POST /v1/chat/completions` is stateless and **the caller owns the loop**: you
package/dist/client.d.ts CHANGED
@@ -39,6 +39,17 @@ export interface RunParams {
39
39
  * The end user this run belongs to. Load-bearing, and the SDK refuses without
40
40
  * it: it is how a run is attributed to a person, therefore how it is metered,
41
41
  * and therefore how Soba knows whose machine it may reach.
42
+ *
43
+ * Soba keys an end user on `(app, user, environment)` and nothing else, so
44
+ * the string has to be stable for as long as the person is. A primary key
45
+ * minted at sign-up is not: reset or move your database and the same person
46
+ * arrives as somebody new, stranding the machine they paired on an id you
47
+ * will never send again. If your own ids are not stable, derive one —
48
+ * `sobaUserId` from `@soba-so/sdk/identity` — and send that. Resolve it on
49
+ * the server from the session; a `user` accepted from the browser lets
50
+ * anyone run on anyone else's machine.
51
+ *
52
+ * https://soba.so/docs/identity
42
53
  */
43
54
  user: string;
44
55
  /** A single user turn. Shorthand for `messages: [{role:"user", content}]`. */
@@ -63,12 +74,37 @@ export interface RunParams {
63
74
  metadata?: Record<string, string>;
64
75
  signal?: AbortSignal;
65
76
  }
77
+ /**
78
+ * A DETACHED run: answered with its id at once, its events and tool calls
79
+ * delivered to `webhook` instead of streamed back. For a run that outlives the
80
+ * request that starts it, like a session somebody works in for an hour. Served
81
+ * only by one of the user's own machines.
82
+ *
83
+ * The app's webhook route verifies each delivery with `verifyWebhook` and
84
+ * answers it with `handleRunWebhook`, passing the same tools.
85
+ */
86
+ export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal"> {
87
+ webhook: {
88
+ url: string;
89
+ secret: string;
90
+ };
91
+ /** `terminal`: open it as the machine owner's own interactive session, in a
92
+ * window they can watch and type into. The machine must allow it. */
93
+ surface?: "headless" | "terminal";
94
+ /** What the terminal window is called. */
95
+ title?: string;
96
+ /** Work in a git worktree on this branch, inside `policy.cwd`. */
97
+ worktree?: {
98
+ branch: string;
99
+ };
100
+ }
66
101
  /**
67
102
  * The Soba client.
68
103
  *
69
104
  * ```js
70
105
  * const soba = new Soba({ apiKey: process.env.SOBA_KEY })
71
- * const run = await soba.run({ user: session.user.id, prompt: "…" })
106
+ * const user = sobaUserId(session.user.username) // see @soba-so/sdk/identity
107
+ * const run = await soba.run({ user, prompt: "…" })
72
108
  * for await (const event of run) …
73
109
  * ```
74
110
  *
@@ -83,16 +119,27 @@ export declare class Soba {
83
119
  readonly users: {
84
120
  /** What this user has: plan, allowance, usage this period, what they connected. */
85
121
  get(id: string): Promise<SobaUser>;
86
- /** Each connected machine, its runtimes, cost classes and live state. */
87
- /** In the SAME shape `<ComputeStatus />` renders, so a server can fetch
88
- * it and pass it to `<SobaProvider initialMachines>`. */
122
+ /**
123
+ * Each connected machine, its runtimes, cost classes and live state, in the
124
+ * SAME shape `<ComputeStatus />` renders, so a server can pass it to
125
+ * `<SobaProvider initialMachines>`.
126
+ *
127
+ * NOT IN A ROOT LAYOUT, AND NOT ON EVERY RENDER. The browser provider reads
128
+ * this itself when it mounts, so a server read only saves a loading frame —
129
+ * and in a layout it is a round trip to Soba on every full load, refresh
130
+ * and revalidating action of every page, which with the default timeout
131
+ * and retries can hold a page for a minute and a half if Soba is slow. To
132
+ * seed one page that shows compute, read it inside `<Suspense>` with a
133
+ * client built for it: `new Soba({ maxRetries: 0, connectTimeoutMs: 1500 })`.
134
+ */
89
135
  compute(id: string): Promise<MachineListItem[]>;
90
136
  };
91
137
  readonly usage: {
92
138
  list(query?: UsageQuery): Promise<UsagePage>;
93
139
  };
94
140
  /** The app's name and connect key — the frame a compute panel renders
95
- * around before it has anything to show. */
141
+ * around before it has anything to show. The browser provider reads it on
142
+ * mount; the same caveat as `users.compute` applies to reading it here. */
96
143
  readonly connect: () => Promise<ConnectContextResponse>;
97
144
  readonly plans: {
98
145
  /**
@@ -103,11 +150,23 @@ export declare class Soba {
103
150
  *
104
151
  * `user` is optional and is the same external id a run is attributed to.
105
152
  * Pass it to learn which plan that person is on.
153
+ *
154
+ * The same caveat as `users.compute`: not in a root layout or on every
155
+ * render. The provider loads plans itself, and only once something on the
156
+ * page reads them.
106
157
  */
107
158
  list(options?: {
108
159
  user?: string;
109
160
  }): Promise<PlanListResponse>;
110
161
  };
162
+ /** Detached runs. See `DetachedRunParams`. */
163
+ readonly runs: {
164
+ start(params: DetachedRunParams): Promise<{
165
+ id: string;
166
+ }>;
167
+ /** Stop a run. For a terminal run, the window closes. */
168
+ cancel(id: string): Promise<void>;
169
+ };
111
170
  constructor(options?: SobaOptions);
112
171
  get baseURL(): string;
113
172
  /**
package/dist/http.d.ts CHANGED
@@ -16,8 +16,17 @@ export interface RequestOptions {
16
16
  body?: unknown;
17
17
  accept?: string;
18
18
  signal?: AbortSignal;
19
- /** Default true. Set false for anything that must happen at most once. */
20
- retry?: boolean;
19
+ /**
20
+ * Default true. Set false for anything that must happen at most once.
21
+ *
22
+ * `"unsent"` for a request that must happen at most once but is worth
23
+ * another try when it PROVABLY never arrived: the connection was refused or
24
+ * the name did not resolve, or the server answered 429 — a refusal made
25
+ * before any work. Not a timeout, not a dropped socket, not a 5xx: each of
26
+ * those can follow a run the gateway already started, and a retry there is
27
+ * a second run, billed as one. `POST /runs` uses this.
28
+ */
29
+ retry?: boolean | "unsent";
21
30
  }
22
31
  export declare function httpRequest(cfg: HttpConfig, opts: RequestOptions): Promise<Response>;
23
32
  export declare function requestJson<T>(cfg: HttpConfig, opts: RequestOptions): Promise<T>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A STABLE `user` FOR SOBA, DERIVED FROM SOMETHING THAT OUTLIVES YOUR DATABASE.
3
+ *
4
+ * Every Soba call takes a `user`, and Soba keys an end user on
5
+ * `(app, user, environment)` and nothing else. It is opaque to us on purpose —
6
+ * whatever string you send names a person, and a string we have never seen
7
+ * names a person we have never met. That is the right behaviour and it is also
8
+ * the trap, because the most obvious thing to send is the worst thing to send.
9
+ *
10
+ * `session.user.id` is usually a random key minted at sign-up and stored in a
11
+ * row. It is stable exactly as long as that row is. Reset the database, migrate
12
+ * it, or stand up a second instance of your app, and the same person signs in,
13
+ * is issued a fresh key, and arrives here as somebody new. The laptop they
14
+ * paired last week stays attached to the id they no longer have, so your Users
15
+ * page shows them twice: once holding the machine, once on metered compute with
16
+ * no runs. Soba cannot detect this. `email` travels with a session but it is a
17
+ * LABEL on that page, not an identifier — two ids carrying one address are two
18
+ * people, drawn on two rows under one name.
19
+ *
20
+ * So: if your own ids are stable, send them and ignore this function. If they
21
+ * are not, derive one from something that is.
22
+ *
23
+ * import { sobaUserId } from "@soba-so/sdk/identity"
24
+ *
25
+ * const user = sobaUserId(session.user.username)
26
+ * await soba.run({ user, prompt })
27
+ *
28
+ * USE THE SAME CALL FOR THE CONNECT SESSION. The person who pairs a laptop and
29
+ * the person a run is attributed to have to be one row, so every Soba boundary
30
+ * in your app goes through here. There is no second formula.
31
+ *
32
+ * SERVER ONLY, WHICH IS WHY THIS IS A SUBPATH. Resolving the id in the browser
33
+ * means accepting it from the browser, and then anyone can claim to be anyone
34
+ * and run on their machine. The import lives at `@soba-so/sdk/identity` rather
35
+ * than on the main entry so that reaching for it in client code fails at the
36
+ * bundler instead of in production — and so the main bundle stays free of node
37
+ * builtins for the runtimes that have none.
38
+ */
39
+ export declare function sobaUserId(stableKey: string): string;
40
+ /**
41
+ * The normalisation, exported so a caller can tell whether two inputs are the
42
+ * same person before deciding what to store.
43
+ *
44
+ * Case and surrounding whitespace only. Anything cleverer — stripping dots,
45
+ * folding plus-addressing, unicode normalisation — would be a guess about what
46
+ * your app considers one account, and a wrong guess merges two people rather
47
+ * than splitting one.
48
+ */
49
+ export declare function normalizeKey(stableKey: string): string;
@@ -0,0 +1 @@
1
+ import{createHash as t}from"node:crypto";function i(r){return`usr_${t("sha256").update(e(r)).digest("hex").slice(0,12)}`}function e(r){return r.trim().toLowerCase()}export{e as normalizeKey,i as sobaUserId};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export { DEFAULT_BASE_URL, type RunParams, Soba, type SobaOptions } from "./client.js";
1
+ export { DEFAULT_BASE_URL, type DetachedRunParams, type RunParams, Soba, type SobaOptions, } from "./client.js";
2
2
  export { SobaError, type SobaErrorCode } from "./errors.js";
3
3
  export { type AgentEvent, type AgentEventType, type CostClass, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
4
4
  export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, MachineListItem, MachineListState, MachineRuntime, PlanListItem, PlanListResponse, PlanSummary, RuntimeListItem, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
5
5
  export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
6
6
  export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
7
7
  export { VERSION } from "./version.js";
8
+ export { handleRunWebhook, type RunWebhookEvent, type ToolCallAnswer, verifyWebhook, } from "./webhooks.js";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var s=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??F.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},F=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),j={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function W(t){let e=j[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function k(t){let e=t.status,n="";try{n=await t.text()}catch{}let r=B(n);return new s({code:r?.code??W(e),type:r?.type,message:r?.message??K(e,t.statusText,n),status:e,retryAfterMs:J(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function B(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let n=e.error;if(!n||typeof n!="object")return null;let r=n;return{message:typeof r.message=="string"&&r.message?r.message:void 0,type:typeof r.type=="string"&&r.type?r.type:void 0,code:G.has(r.code)?r.code:void 0}}var G=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function K(t,e,n){let r=n.trim().slice(0,200),o=r?`: ${r}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function J(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,n-Date.now())}function f(t){if(t instanceof s)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new s({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new s({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var w="0.0.7";var H=8e3,Y=3e4;async function g(t,e){let n=Q(t.baseURL,e.path,e.query),r=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o;for(let i=0;i<r;i++){i>0&&await Z(X(i,o),e.signal);let a=new AbortController,c=S(e.signal,a),m=setTimeout(()=>{a.abort(new s({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),d;try{d=await t.fetch(n,{method:e.method,headers:z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:a.signal})}catch(l){if(c(),o=f(a.signal.aborted?a.signal.reason??l:l),!o.retryable||i===r-1)throw o;continue}finally{clearTimeout(m)}if(d.ok)return d;if(c(),o=await k(d),!o.retryable||i===r-1)throw o}throw o??new s({code:"unknown",message:"The request failed with no error."})}async function C(t,e){let n=await g(t,{accept:"application/json",...e}),r=await n.text();if(r)try{return JSON.parse(r)}catch(o){throw new s({code:"server_error",message:`Soba returned a body that is not JSON: ${r.slice(0,200)}`,status:n.status,cause:o})}}function z(t,e){let n={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":V(),...t.headers};return e.body!==void 0&&(n["content-type"]="application/json"),n}function V(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${w}${t}`}function Q(t,e,n){let r=t.replace(/\/+$/,""),o=new URL(`${r}${e.startsWith("/")?e:`/${e}`}`);for(let[i,a]of Object.entries(n??{}))a!==void 0&&o.searchParams.set(i,String(a));return o.toString()}function X(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Y);let n=Math.min(H,500*2**(t-1));return Math.round(n*(.5+Math.random()/2))}function Z(t,e){return new Promise((n,r)=>{if(e?.aborted)return r(f(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",i),n()},t);function i(){clearTimeout(o),r(f(e?.reason))}e?.addEventListener("abort",i,{once:!0})})}function S(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let n=()=>e.abort(t.reason);return t.addEventListener("abort",n,{once:!0}),()=>t.removeEventListener("abort",n)}var _=["open-weights","frontier"];function ee(t){return _.includes(t)}function te(t){return t==="user-hardware"||t==="user-subscription"||t==="open-weights"||t==="frontier"}function T(t){return t.type==="done"||t.type==="error"}var ne=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var P=new Set(["delta","thinking","status","done","error"]);function R(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let n=e,r=u(n.runId);if(typeof n.type=="string"&&P.has(n.type)){let o=I(n);return o?{kind:"event",runId:r,event:o}:null}switch(n.type){case"event":{if(!n.event||typeof n.event!="object")return null;let o=I(n.event);return o?{kind:"event",runId:r,event:o}:null}case"usage":{let o=U(n.usage);return o?{kind:"usage",runId:r,usage:o}:null}case"tool_call":{let o=u(n.callId),i=u(n.name);return!o||!i?null:{kind:"tool_call",runId:r,callId:o,name:i,input:n.input}}case"approval_request":{let o=u(n.approvalId),i=u(n.tool);return!o||!i?null:{kind:"approval",runId:r,approvalId:o,tool:i,input:n.input,cwd:u(n.cwd)}}default:return null}}function I(t){let e=typeof t.type=="string"&&P.has(t.type)?t.type:null;if(!e)return null;let n={type:e},r=u(t.text);r!==void 0&&(n.text=r);let o=u(t.message);o!==void 0&&(n.message=o);let i=u(t.tool);i!==void 0&&(n.tool=i);let a=u(t.tier);a!==void 0&&(n.tier=a);let c=U(t.usage);return c&&(n.usage=c),n}function U(t){if(!t||typeof t!="object")return null;let e=t,n={model:u(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},r=u(e.costClass);r!==void 0&&(n.costClass=r);let o=u(e.provider);return o!==void 0&&(n.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(n.costMicros=e.costMicros),n}function u(t){return typeof t=="string"&&t.length>0?t:void 0}function y(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*O(t){let e=t.body;if(!e)throw new s({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let n=t.headers.get("content-type")??"",r=n.includes("ndjson")||n.includes("jsonlines"),o=e.getReader(),i=new TextDecoder,a="",c=[];try{for(;;){let d=await o.read();if(d.done)break;for(a+=i.decode(d.value,{stream:!0});;){let l=a.indexOf(`
2
- `);if(l===-1)break;let b=q(a.slice(0,l));if(a=a.slice(l+1),r){let p=b.trim()?R(b):null;p&&(yield p);continue}if(b===""){let p=c.join(`
3
- `);if(c=[],p==="[DONE]")return;let A=p?R(p):null;A&&(yield A);continue}M(b,c)}}!r&&a&&M(q(a),c);let m=r?a.trim():c.join(`
4
- `);if(m&&m!=="[DONE]"){let d=R(m);d&&(yield d)}}finally{o.cancel().catch(()=>{})}}function M(t,e){if(t.startsWith(":"))return;let n=t.indexOf(":");if(n===-1||t.slice(0,n)!=="data")return;let r=t.slice(n+1);e.push(r.startsWith(" ")?r.slice(1):r)}function q(t){return t.endsWith("\r")?t.slice(0,-1):t}function re(t){return t}function L(t){let e=new Map;for(let n of t??[]){if(!n?.name)throw new s({code:"invalid_request",message:"Every tool needs a name."});if(typeof n.execute!="function")throw new s({code:"invalid_request",message:`Tool "${n.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(n.name))throw new s({code:"invalid_request",message:`Two tools are named "${n.name}". Tool names must be unique within a run.`});e.set(n.name,n)}return e}function D(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function N(t){if(t===void 0)return null;if(typeof t=="string")return t;try{return JSON.parse(JSON.stringify(t))}catch{return String(t)}}function v(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var h=class{#e;#n;#r;#a=!1;#t;#o=new AbortController;#s=new AbortController;#i=new Set;#u;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#u=S(e.signal,this.#o)}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#d()}async text(){let e="";for await(let n of this)if(n.type==="delta"&&n.text&&(e+=n.text),n.type==="error")throw new s({code:"run_failed",message:n.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#o.abort(new s({code:"canceled",message:"The run was cancelled."})),this.#s.abort(new s({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await g(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#d(){if(this.#a)throw new s({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#a=!0;let e=!1;try{for await(let n of O(this.#t.response)){if(n.runId&&!this.#e&&(this.#e=n.runId),n.kind==="event"){let r=n.event;if(r.tier&&(this.#n=r.tier),r.usage&&(this.#r=r.usage),e=T(r),yield r,e)return;continue}if(n.kind==="usage"){this.#r=n.usage;continue}this.#l(this.#p(n))}throw new s({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#s.abort(new s({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#i]),this.#o.abort(),this.#u()}}#l(e){this.#i.add(e),e.finally(()=>this.#i.delete(e)).catch(()=>{})}async#p(e){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#g(e)}async#f(e){let n=this.#t.tools.get(e.name),r,o=!1;if(!n)r=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let i={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#s.signal};try{r=await n.execute(e.input,i)}catch(a){r=v(a),o=!0}}await this.#c(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:N(r),isError:o})}async#g(e){let n="deny",r;if(!this.#t.approve)r="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),n=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){r=v(o)}await this.#c(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:n,...r?{reason:r}:{}})}async#c(e,n){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",n);return}try{await(await g(this.#t.config,{method:"POST",path:e,body:n})).text()}catch(r){this.#t.onWarning(`Could not deliver an answer to ${e}.`,f(r))}}};var $="https://api.soba.so/v1",oe=3e4,se=2,x=class{#e;#n;users;usage;connect;plans;constructor(e={}){let n=e.apiKey??E("SOBA_KEY")??E("SOBA_API_KEY");if(!n)throw new s({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(ie()&&!e.dangerouslyAllowBrowser)throw new s({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#n=e.onWarning??(()=>{}),this.#e={apiKey:n,baseURL:e.baseURL??E("SOBA_BASE_URL")??$,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??oe,maxRetries:e.maxRetries??se},typeof this.#e.fetch!="function")throw new s({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:r=>this.request("GET",`/users/${encodeURIComponent(r)}`),compute:async r=>(await this.request("GET",`/users/${encodeURIComponent(r)}/compute`)).machines},this.usage={list:r=>this.request("GET","/usage",{query:{...r}})},this.connect=()=>this.request("GET","/connect"),this.plans={list:r=>this.request("GET","/plans",{...r?.user?{query:{user:r.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new s({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new s({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let n=L(e.tools),r={user:e.user,model:e.model??"auto"};e.prompt&&(r.prompt=e.prompt),e.messages?.length&&(r.messages=e.messages),e.system&&(r.system=e.system),e.runtime&&(r.runtime=e.runtime),n.size&&(r.tools=D(n)),e.route&&(r.route=e.route),e.policy&&(r.policy=e.policy),e.metadata&&(r.metadata=e.metadata);let o=await g(this.#e,{method:"POST",path:"/runs",body:r,accept:"text/event-stream",signal:e.signal});return new h({config:this.#e,response:o,user:e.user,tools:n,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,n,r={}){return C(this.#e,{method:e,path:n,...r})}};function E(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function ie(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}export{$ as DEFAULT_BASE_URL,_ as METERED_CLASSES,h as Run,x as Soba,s as SobaError,ne as TOOL_SLUGS,w as VERSION,te as isCostClass,ee as isMetered,T as isTerminal,re as tool};
1
+ var i=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??F.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},F=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),W={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function B(t){let e=W[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function P(t){let e=t.status,r="";try{r=await t.text()}catch{}let n=G(r);return new i({code:n?.code??B(e),type:n?.type,message:n?.message??H(e,t.statusText,r),status:e,retryAfterMs:J(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function G(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let n=r;return{message:typeof n.message=="string"&&n.message?n.message:void 0,type:typeof n.type=="string"&&n.type?n.type:void 0,code:K.has(n.code)?n.code:void 0}}var K=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function H(t,e,r){let n=r.trim().slice(0,200),o=n?`: ${n}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function J(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function h(t){if(t instanceof i)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new i({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new i({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var R="0.0.8";var z=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function Y(t){if(t.code==="rate_limited")return!0;if(t.code!=="connection")return!1;let e=t.cause;for(let r=0;e&&r<4;r++){let n=e.code;if(typeof n=="string"&&z.has(n))return!0;e=e.cause}return!1}var V=8e3,Q=3e4;async function y(t,e){let r=ee(t.baseURL,e.path,e.query),n=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o,s=a=>a.retryable&&(e.retry!=="unsent"||Y(a));for(let a=0;a<n;a++){a>0&&await ne(te(a,o),e.signal);let u=new AbortController,g=x(e.signal,u),f=setTimeout(()=>{u.abort(new i({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),l;try{l=await t.fetch(r,{method:e.method,headers:X(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(g(),o=h(u.signal.aborted?u.signal.reason??p:p),!s(o)||a===n-1)throw o;continue}finally{clearTimeout(f)}if(l.ok)return l;if(g(),o=await P(l),!s(o)||a===n-1)throw o}throw o??new i({code:"unknown",message:"The request failed with no error."})}async function E(t,e){let r=await y(t,{accept:"application/json",...e}),n=await r.text();if(n)try{return JSON.parse(n)}catch(o){throw new i({code:"server_error",message:`Soba returned a body that is not JSON: ${n.slice(0,200)}`,status:r.status,cause:o})}}function X(t,e){let r={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":Z(),...t.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function Z(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${R}${t}`}function ee(t,e,r){let n=t.replace(/\/+$/,""),o=new URL(`${n}${e.startsWith("/")?e:`/${e}`}`);for(let[s,a]of Object.entries(r??{}))a!==void 0&&o.searchParams.set(s,String(a));return o.toString()}function te(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Q);let r=Math.min(V,500*2**(t-1));return Math.round(r*(.5+Math.random()/2))}function ne(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(h(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t);function s(){clearTimeout(o),n(h(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function x(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let r=()=>e.abort(t.reason);return t.addEventListener("abort",r,{once:!0}),()=>t.removeEventListener("abort",r)}var q=["open-weights","frontier"];function re(t){return q.includes(t)}function oe(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function k(t){return t.type==="done"||t.type==="error"}var se=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var L=new Set(["delta","thinking","status","done","error"]);function A(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,n=c(r.runId);if(typeof r.type=="string"&&L.has(r.type)){let o=U(r);return o?{kind:"event",runId:n,event:o}:null}switch(r.type){case"event":{if(!r.event||typeof r.event!="object")return null;let o=U(r.event);return o?{kind:"event",runId:n,event:o}:null}case"usage":{let o=D(r.usage);return o?{kind:"usage",runId:n,usage:o}:null}case"tool_call":{let o=c(r.callId),s=c(r.name);return!o||!s?null:{kind:"tool_call",runId:n,callId:o,name:s,input:r.input}}case"approval_request":{let o=c(r.approvalId),s=c(r.tool);return!o||!s?null:{kind:"approval",runId:n,approvalId:o,tool:s,input:r.input,cwd:c(r.cwd)}}default:return null}}function U(t){let e=typeof t.type=="string"&&L.has(t.type)?t.type:null;if(!e)return null;let r={type:e},n=c(t.text);n!==void 0&&(r.text=n);let o=c(t.message);o!==void 0&&(r.message=o);let s=c(t.tool);s!==void 0&&(r.tool=s);let a=c(t.tier);a!==void 0&&(r.tier=a);let u=D(t.usage);return u&&(r.usage=u),r}function D(t){if(!t||typeof t!="object")return null;let e=t,r={model:c(e.model)??"unknown",inputTokens:b(e.inputTokens),outputTokens:b(e.outputTokens),cacheReadTokens:b(e.cacheReadTokens),cacheWriteTokens:b(e.cacheWriteTokens),webSearches:b(e.webSearches)},n=c(e.costClass);n!==void 0&&(r.costClass=n);let o=c(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function c(t){return typeof t=="string"&&t.length>0?t:void 0}function b(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*$(t){let e=t.body;if(!e)throw new i({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let r=t.headers.get("content-type")??"",n=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,a="",u=[];try{for(;;){let f=await o.read();if(f.done)break;for(a+=s.decode(f.value,{stream:!0});;){let l=a.indexOf(`
2
+ `);if(l===-1)break;let p=N(a.slice(0,l));if(a=a.slice(l+1),n){let d=p.trim()?A(p):null;d&&(yield d);continue}if(p===""){let d=u.join(`
3
+ `);if(u=[],d==="[DONE]")return;let m=d?A(d):null;m&&(yield m);continue}O(p,u)}}!n&&a&&O(N(a),u);let g=n?a.trim():u.join(`
4
+ `);if(g&&g!=="[DONE]"){let f=A(g);f&&(yield f)}}finally{o.cancel().catch(()=>{})}}function O(t,e){if(t.startsWith(":"))return;let r=t.indexOf(":");if(r===-1||t.slice(0,r)!=="data")return;let n=t.slice(r+1);e.push(n.startsWith(" ")?n.slice(1):n)}function N(t){return t.endsWith("\r")?t.slice(0,-1):t}function ie(t){return t}function w(t){let e=new Map;for(let r of t??[]){if(!r?.name)throw new i({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new i({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new i({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function _(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function v(t){if(t===void 0)return null;if(typeof t=="string")return t;try{return JSON.parse(JSON.stringify(t))}catch{return String(t)}}function S(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var T=class{#e;#n;#r;#a=!1;#t;#o=new AbortController;#s=new AbortController;#i=new Set;#u;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#u=x(e.signal,this.#o)}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#d()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new i({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#o.abort(new i({code:"canceled",message:"The run was cancelled."})),this.#s.abort(new i({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await y(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#d(){if(this.#a)throw new i({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#a=!0;let e=!1;try{for await(let r of $(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.kind==="event"){let n=r.event;if(n.tier&&(this.#n=n.tier),n.usage&&(this.#r=n.usage),e=k(n),yield n,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#l(this.#p(r))}throw new i({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#s.abort(new i({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#i]),this.#o.abort(),this.#u()}}#l(e){this.#i.add(e),e.finally(()=>this.#i.delete(e)).catch(()=>{})}async#p(e){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#g(e)}async#f(e){let r=this.#t.tools.get(e.name),n,o=!1;if(!r)n=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#s.signal};try{n=await r.execute(e.input,s)}catch(a){n=S(a),o=!0}}await this.#c(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:v(n),isError:o})}async#g(e){let r="deny",n;if(!this.#t.approve)n="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){n=S(o)}await this.#c(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...n?{reason:n}:{}})}async#c(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await y(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(n){this.#t.onWarning(`Could not deliver an answer to ${e}.`,h(n))}}};var j="https://api.soba.so/v1",ae=3e4,ue=2,I=class{#e;#n;users;usage;connect;plans;runs;constructor(e={}){let r=e.apiKey??C("SOBA_KEY")??C("SOBA_API_KEY");if(!r)throw new i({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(ce()&&!e.dangerouslyAllowBrowser)throw new i({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#n=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??C("SOBA_BASE_URL")??j,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??ae,maxRetries:e.maxRetries??ue},typeof this.#e.fetch!="function")throw new i({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:n=>this.request("GET",`/users/${encodeURIComponent(n)}`),compute:async n=>(await this.request("GET",`/users/${encodeURIComponent(n)}/compute`)).machines},this.usage={list:n=>this.request("GET","/usage",{query:{...n}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async n=>{if(!n.user)throw new i({code:"user_required",status:422,message:"`user` is required."});let o=w(n.tools),s={user:n.user,model:n.model??"auto",detached:!0,webhook:n.webhook};return n.prompt&&(s.prompt=n.prompt),n.messages?.length&&(s.messages=n.messages),n.system&&(s.system=n.system),n.runtime&&(s.runtime=n.runtime),o.size&&(s.tools=_(o)),n.route&&(s.route=n.route),n.policy&&(s.policy=n.policy),n.metadata&&(s.metadata=n.metadata),n.surface&&(s.surface=n.surface),n.title&&(s.title=n.title),n.worktree&&(s.worktree=n.worktree),{id:(await E(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"})).id}},cancel:async n=>{await this.request("POST",`/runs/${encodeURIComponent(n)}/cancel`,{body:{}})}},this.plans={list:n=>this.request("GET","/plans",{...n?.user?{query:{user:n.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new i({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new i({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=w(e.tools),n={user:e.user,model:e.model??"auto"};e.prompt&&(n.prompt=e.prompt),e.messages?.length&&(n.messages=e.messages),e.system&&(n.system=e.system),e.runtime&&(n.runtime=e.runtime),r.size&&(n.tools=_(r)),e.route&&(n.route=e.route),e.policy&&(n.policy=e.policy),e.metadata&&(n.metadata=e.metadata);let o=await y(this.#e,{method:"POST",path:"/runs",body:n,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new T({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,r,n={}){return E(this.#e,{method:e,path:r,...n})}};function C(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function ce(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}var de=300;function le(t){return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,"0")).join("")}function pe(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}async function fe(t,e,r,n={}){let o=Object.fromEntries((e??"").split(",").map(m=>{let M=m.indexOf("=");return[m.slice(0,M).trim(),m.slice(M+1).trim()]})),s=Number(o.t),a=o.v1??"",u=m=>new i({code:"unauthorized",status:401,message:m});if(!Number.isFinite(s)||!a)throw u("Missing or malformed Soba-Signature.");let g=Math.floor((n.now??Date.now())/1e3);if(Math.abs(g-s)>(n.toleranceSeconds??de))throw u("This delivery is too old to accept.");let f=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=le(await globalThis.crypto.subtle.sign("HMAC",f,new TextEncoder().encode(`${s}.${t}`)));if(!pe(l,a))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(t)}catch{throw new i({code:"invalid_request",message:"The delivery is not JSON."})}let d=p;if(d.type!=="run.event"&&d.type!=="run.tool_call"||typeof d.run_id!="string")throw new i({code:"invalid_request",message:"Not a Soba run delivery."});return d}async function ge(t,e){if(t.type!=="run.tool_call")return null;let{call_id:r,name:n,input:o}=t.data,s=w(e.tools).get(n);if(!s)return{result:`Unknown tool: ${n}`,is_error:!0};try{let a=await s.execute(o??{},{runId:t.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:v(a)}}catch(a){return{result:S(a),is_error:!0}}}export{j as DEFAULT_BASE_URL,q as METERED_CLASSES,T as Run,I as Soba,i as SobaError,se as TOOL_SLUGS,R as VERSION,ge as handleRunWebhook,oe as isCostClass,re as isMetered,k as isTerminal,ie as tool,fe as verifyWebhook};
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * user-hardware the user's own silicon. Free to everyone
5
5
  * user-subscription a plan the user already pays for. Free to the broker
6
+ * user-api-key the user's own provider key. Free to the broker, billed
7
+ * to the user by their provider
6
8
  * open-weights an open model on GPU bought wholesale. METERED
7
9
  * frontier a frontier provider API. METERED
8
10
  */
9
- export type CostClass = "user-hardware" | "user-subscription" | "open-weights" | "frontier";
11
+ export type CostClass = "user-hardware" | "user-subscription" | "user-api-key" | "open-weights" | "frontier";
10
12
  /** The classes somebody has to be billed for. The user-* classes are already paid. */
11
13
  export declare const METERED_CLASSES: readonly ["open-weights", "frontier"];
12
14
  export declare function isMetered(c: CostClass): boolean;
@@ -106,6 +106,9 @@ export interface PlanListItem {
106
106
  interval: string;
107
107
  unit: string;
108
108
  included_units: number | null;
109
+ /** What happens past the allowance: `stop` or `meter`. The difference
110
+ * between a wall and a bill, and not guessable from the price. */
111
+ overage: string;
109
112
  /** Dollars per unit past the allowance. */
110
113
  overage_per_unit: number;
111
114
  trial_days: number;
@@ -113,6 +116,17 @@ export interface PlanListItem {
113
116
  route_allow: string[] | null;
114
117
  /** Whether it can be taken without a payment rail. */
115
118
  selectable: boolean;
119
+ /**
120
+ * What connecting their own compute earns: `none` | `unlimited` | `included`
121
+ * | `credit`. Here for the same reason every other field is: these objects
122
+ * are handed straight to `<SobaProvider initialPlans>` and rendered, so the
123
+ * shape has to be the component's, not a report's.
124
+ */
125
+ connected_reward: string;
126
+ /** The larger allowance an `included` reward grants. Null otherwise. */
127
+ connected_included_units: number | null;
128
+ /** Dollars back per interval on a `credit` reward. Zero otherwise. */
129
+ connected_credit: number;
116
130
  }
117
131
  export interface CurrentPlanItem {
118
132
  plan_id: string;
@@ -162,6 +176,10 @@ export type MachineListState = "online" | "busy" | "offline";
162
176
  export interface MachineListItem {
163
177
  id: string;
164
178
  label: string;
179
+ /** The reported hostname, which `label` already falls back to. Carried so a
180
+ * renderer can tell a name somebody chose from a DNS label and tidy only
181
+ * the second — see `machineName` in connect-ui. */
182
+ name?: string | null;
165
183
  platform: string | null;
166
184
  arch: string | null;
167
185
  version: string | null;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.7";
1
+ export declare const VERSION = "0.0.8";
@@ -0,0 +1,56 @@
1
+ import type { AgentEvent } from "./protocol.js";
2
+ import { type ToolDefinition } from "./tools.js";
3
+ /** A delivery, after its signature has been checked. */
4
+ export type RunWebhookEvent = {
5
+ type: "run.event";
6
+ run_id: string;
7
+ created: number;
8
+ metadata?: Record<string, string>;
9
+ data: {
10
+ event: AgentEvent & {
11
+ type: string;
12
+ turn?: {
13
+ text: string;
14
+ };
15
+ };
16
+ };
17
+ } | {
18
+ type: "run.tool_call";
19
+ run_id: string;
20
+ created: number;
21
+ metadata?: Record<string, string>;
22
+ data: {
23
+ call_id: string;
24
+ name: string;
25
+ input: Record<string, unknown>;
26
+ };
27
+ };
28
+ /** What a tool call is answered with. */
29
+ export interface ToolCallAnswer {
30
+ result: unknown;
31
+ is_error?: boolean;
32
+ }
33
+ /**
34
+ * Check a delivery's `Soba-Signature` against the secret the run was started
35
+ * with, and parse it. Throws on a bad signature, a stale timestamp (replays
36
+ * older than five minutes), or a body that is not a delivery.
37
+ *
38
+ * Pass the body exactly as it arrived: re-serialising parsed JSON changes the
39
+ * bytes and the signature with them.
40
+ */
41
+ export declare function verifyWebhook(rawBody: string, header: string | null | undefined, secret: string, options?: {
42
+ toleranceSeconds?: number;
43
+ now?: number;
44
+ }): Promise<RunWebhookEvent>;
45
+ /**
46
+ * Answer a verified delivery. A tool call runs the named tool from `tools` and
47
+ * resolves to its result; anything else resolves to `null`.
48
+ *
49
+ * A tool that throws is answered as an error the agent can read and recover
50
+ * from, never as a failed request: a 500 here would be retried, and the tool
51
+ * would run again.
52
+ */
53
+ export declare function handleRunWebhook(event: RunWebhookEvent, options: {
54
+ tools: readonly ToolDefinition[];
55
+ user?: string;
56
+ }): Promise<ToolCallAnswer | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/sdk",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "One request that comes back finished, executed on compute the end user owns.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://soba.so/docs/runs",
@@ -11,6 +11,10 @@
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
13
  "default": "./dist/index.mjs"
14
+ },
15
+ "./identity": {
16
+ "types": "./dist/identity.d.ts",
17
+ "default": "./dist/identity.mjs"
14
18
  }
15
19
  },
16
20
  "files": [