@myapihq/sdk 1.3.6 → 1.3.11

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/dist/email.d.ts CHANGED
@@ -106,6 +106,10 @@ export declare function createMailbox(apiKey: string, domain: string, username:
106
106
  address: string;
107
107
  created_at: string;
108
108
  }>;
109
+ export declare function deleteMailbox(apiKey: string, address: string): Promise<void>;
110
+ export declare function mailServerResync(apiKey: string, orgId: string, domain: string): Promise<{
111
+ ok: boolean;
112
+ } & Record<string, unknown>>;
109
113
  export declare function listMailboxes(apiKey: string, options?: {
110
114
  domain?: string;
111
115
  filter?: 'unassigned';
package/dist/email.js CHANGED
@@ -5,6 +5,8 @@ exports.verifyEmail = verifyEmail;
5
5
  exports.verifyBulk = verifyBulk;
6
6
  exports.getVerifyJob = getVerifyJob;
7
7
  exports.createMailbox = createMailbox;
8
+ exports.deleteMailbox = deleteMailbox;
9
+ exports.mailServerResync = mailServerResync;
8
10
  exports.listMailboxes = listMailboxes;
9
11
  exports.activateSending = activateSending;
10
12
  exports.setForwarding = setForwarding;
@@ -107,6 +109,20 @@ async function getVerifyJob(apiKey, orgId, jobId) {
107
109
  async function createMailbox(apiKey, domain, username, displayName) {
108
110
  return (0, client_1.request)('POST', `${config_1.EMAIL_BASE}/email/mailboxes/create`, apiKey, { domain, username, display_name: displayName });
109
111
  }
112
+ // Backend (2026-06-06): idempotent — 204 whether the mailbox existed or not.
113
+ // Returns 409 MAILBOX_IN_USE when an active/paused campaign still sends from
114
+ // this address; pause/delete the campaign first.
115
+ async function deleteMailbox(apiKey, address) {
116
+ return (0, client_1.request)('DELETE', `${config_1.EMAIL_BASE}/email/mailboxes/${encodeURIComponent(address)}`, apiKey);
117
+ }
118
+ // Backend (2026-06-06): re-run Stalwart provisioning for a domain. Use when
119
+ // mailbox-create on a domain returns DOMAIN_NOT_MAIL_READY /
120
+ // MAILBOX_PROVISION_FAILED, or when the inbox endpoint returns
121
+ // "mail server error" on a domain whose `email_infra_ready` flag is stale.
122
+ // Idempotent.
123
+ async function mailServerResync(apiKey, orgId, domain) {
124
+ return (0, client_1.request)('POST', `${config_1.EMAIL_BASE}/email/orgs/${encodeURIComponent(orgId)}/domains/${encodeURIComponent(domain)}/mail-server-resync`, apiKey);
125
+ }
110
126
  async function listMailboxes(apiKey, options) {
111
127
  const query = new URLSearchParams();
112
128
  if (options?.domain)
package/dist/funnel.d.ts CHANGED
@@ -4,14 +4,36 @@ export interface Funnel {
4
4
  id: string;
5
5
  org_id: string;
6
6
  name?: string;
7
+ org_webhook_id?: string;
7
8
  created_at: string;
8
9
  updated_at: string;
9
10
  }
11
+ export interface FormFieldSpec {
12
+ name: string;
13
+ required?: boolean;
14
+ oneof?: string[];
15
+ max_length?: number;
16
+ regex?: string;
17
+ }
18
+ export interface FormBinding {
19
+ slug: string;
20
+ destination: string;
21
+ fields?: FormFieldSpec[];
22
+ honeypot_field?: string;
23
+ rate_limit?: {
24
+ per_minute: number;
25
+ };
26
+ }
10
27
  export interface CreateFunnelResponse {
11
28
  funnel: Funnel;
12
29
  subdomain_url?: string;
13
30
  domain_url?: string;
14
31
  }
32
+ export interface GetFunnelResponse {
33
+ funnel: Funnel;
34
+ subdomain_url?: string;
35
+ domain_url?: string;
36
+ }
15
37
  export interface VerifyResult {
16
38
  pages?: Array<{
17
39
  slug: string;
@@ -28,7 +50,7 @@ export interface VerifyTest {
28
50
  export declare function createFunnel(apiKey: string, orgId: string, opts?: {
29
51
  name?: string;
30
52
  }): Promise<CreateFunnelResponse>;
31
- export declare function getFunnel(apiKey: string, orgId: string, funnelId: string): Promise<Funnel>;
53
+ export declare function getFunnel(apiKey: string, orgId: string, funnelId: string): Promise<GetFunnelResponse>;
32
54
  export declare function listFunnels(apiKey: string, orgId: string): Promise<Funnel[]>;
33
55
  export declare function deleteFunnel(apiKey: string, orgId: string, funnelId: string): Promise<void>;
34
56
  export declare function pushFunnelPage(apiKey: string, orgId: string, funnelId: string, payload: {
@@ -67,3 +89,6 @@ export interface PublishResult {
67
89
  published_url: string;
68
90
  }
69
91
  export declare function publishFiles(apiKey: string, orgId: string, funnelId: string, files: PublishFile[], opts?: PublishOptions): Promise<PublishResult>;
92
+ export declare function createFormBinding(apiKey: string, orgId: string, funnelId: string, binding: FormBinding): Promise<FormBinding>;
93
+ export declare function listFormBindings(apiKey: string, orgId: string, funnelId: string): Promise<FormBinding[]>;
94
+ export declare function deleteFormBinding(apiKey: string, orgId: string, funnelId: string, slug: string): Promise<void>;
package/dist/funnel.js CHANGED
@@ -9,6 +9,9 @@ exports.pushFunnelPage = pushFunnelPage;
9
9
  exports.listFunnelPages = listFunnelPages;
10
10
  exports.verifyFunnel = verifyFunnel;
11
11
  exports.publishFiles = publishFiles;
12
+ exports.createFormBinding = createFormBinding;
13
+ exports.listFormBindings = listFormBindings;
14
+ exports.deleteFormBinding = deleteFormBinding;
12
15
  const client_1 = require("./client");
13
16
  const config_1 = require("./config");
14
17
  exports.EXPOSES = [
@@ -20,6 +23,12 @@ exports.EXPOSES = [
20
23
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
21
24
  'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
22
25
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
26
+ // Backend (2026-06-03): canonical funnel-form proxy lands per-slug
27
+ // bindings — destination (webhook/workflow), fields, honeypot, rate
28
+ // limit. See docs/cross-repo-prompts/cli-funnel-form-canonical-followup-2026-06-04.md.
29
+ 'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
30
+ 'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
31
+ 'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}/forms/{slug}',
23
32
  ];
24
33
  async function createFunnel(apiKey, orgId, opts) {
25
34
  // v2: accept optional name for N-funnels-per-org. Backend rejects names
@@ -27,7 +36,14 @@ async function createFunnel(apiKey, orgId, opts) {
27
36
  return (0, client_1.request)('POST', `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels`, apiKey, opts?.name ? { name: opts.name } : {});
28
37
  }
29
38
  async function getFunnel(apiKey, orgId, funnelId) {
30
- return (0, client_1.request)('GET', `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}`, apiKey);
39
+ // Backend has wrapped GET responses since 2026-04 (`{funnel, subdomain_url, domain_url}`).
40
+ // Older builds returned a flat Funnel — normalize to the envelope so
41
+ // callers can rely on the typed shape.
42
+ const raw = await (0, client_1.request)('GET', `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}`, apiKey);
43
+ if (raw && typeof raw === 'object' && 'funnel' in raw && raw.funnel) {
44
+ return raw;
45
+ }
46
+ return { funnel: raw };
31
47
  }
32
48
  async function listFunnels(apiKey, orgId) {
33
49
  return (0, client_1.request)('GET', `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels`, apiKey);
@@ -80,3 +96,17 @@ async function publishFiles(apiKey, orgId, funnelId, files, opts = {}) {
80
96
  }
81
97
  return result.data;
82
98
  }
99
+ // ── Form bindings (canonical funnel-form proxy) ────────────────────────────
100
+ function formsBase(orgId, funnelId) {
101
+ return `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/forms`;
102
+ }
103
+ async function createFormBinding(apiKey, orgId, funnelId, binding) {
104
+ return (0, client_1.request)('POST', formsBase(orgId, funnelId), apiKey, binding);
105
+ }
106
+ async function listFormBindings(apiKey, orgId, funnelId) {
107
+ const res = await (0, client_1.request)('GET', formsBase(orgId, funnelId), apiKey);
108
+ return Array.isArray(res) ? res : (res?.forms ?? []);
109
+ }
110
+ async function deleteFormBinding(apiKey, orgId, funnelId, slug) {
111
+ return (0, client_1.request)('DELETE', `${formsBase(orgId, funnelId)}/${encodeURIComponent(slug)}`, apiKey);
112
+ }
package/dist/hq.d.ts CHANGED
@@ -74,6 +74,12 @@ export type FreeTierEntry = {
74
74
  };
75
75
  export declare function getFreeTier(apiKey: string): Promise<FreeTierEntry[] | null>;
76
76
  export declare function getAccount(apiKey: string): Promise<AccountInfo>;
77
+ export declare function getMailingAddress(apiKey: string): Promise<{
78
+ mailing_address: string | null;
79
+ }>;
80
+ export declare function setMailingAddress(apiKey: string, mailingAddress: string): Promise<{
81
+ mailing_address: string;
82
+ }>;
77
83
  export interface CreateApiKeyOptions {
78
84
  grants?: Grants;
79
85
  orgId?: string;
package/dist/hq.js CHANGED
@@ -7,6 +7,8 @@ exports.verifyCode = verifyCode;
7
7
  exports.upgradeAccount = upgradeAccount;
8
8
  exports.getFreeTier = getFreeTier;
9
9
  exports.getAccount = getAccount;
10
+ exports.getMailingAddress = getMailingAddress;
11
+ exports.setMailingAddress = setMailingAddress;
10
12
  exports.createApiKey = createApiKey;
11
13
  exports.listApiKeys = listApiKeys;
12
14
  exports.revokeApiKey = revokeApiKey;
@@ -40,6 +42,11 @@ exports.EXPOSES = [
40
42
  'POST /hq/account/keys/revoke-all',
41
43
  'DELETE /hq/account/delete/key/{key_id}',
42
44
  'PATCH /hq/account/spend-cap',
45
+ // Backend (2026-06-06): account-scoped mailing address — required for
46
+ // transactional email per CAN-SPAM. PATCH sets, GET reads (null when
47
+ // unset). Hard-gates POST /email/send with MAILING_ADDRESS_REQUIRED.
48
+ 'PATCH /hq/account/mailing-address',
49
+ 'GET /hq/account/mailing-address',
43
50
  'POST /hq/orgs',
44
51
  'GET /hq/orgs',
45
52
  'GET /hq/orgs/{org_id}',
@@ -80,6 +87,14 @@ async function getFreeTier(apiKey) {
80
87
  async function getAccount(apiKey) {
81
88
  return (0, client_1.request)('GET', `${config_1.HQ_BASE}/hq/account/me`, apiKey);
82
89
  }
90
+ // Mailing address — required for transactional `email send` (CAN-SPAM).
91
+ // `getMailingAddress` returns `null` for `mailing_address` when unset.
92
+ async function getMailingAddress(apiKey) {
93
+ return (0, client_1.request)('GET', `${config_1.HQ_BASE}/hq/account/mailing-address`, apiKey);
94
+ }
95
+ async function setMailingAddress(apiKey, mailingAddress) {
96
+ return (0, client_1.request)('PATCH', `${config_1.HQ_BASE}/hq/account/mailing-address`, apiKey, { mailing_address: mailingAddress });
97
+ }
83
98
  // Mints a manual API key. The requested (grants, org, spend cap) must be a
84
99
  // subset of the calling key's authority — the backend rejects escalation
85
100
  // with 403 SCOPE_FORBIDDEN. The full `api_key` is in the response ONCE.
package/dist/llm.d.ts CHANGED
@@ -12,15 +12,16 @@ export interface CompleteRequest {
12
12
  temperature?: number;
13
13
  stop?: string[];
14
14
  }
15
+ export interface RawUsage {
16
+ input_tokens: number;
17
+ output_tokens?: number;
18
+ cost_cents: number;
19
+ }
15
20
  export interface CompleteResponse {
16
21
  model: string;
17
22
  content: string;
18
23
  finish_reason: string;
19
- usage: {
20
- input_tokens: number;
21
- output_tokens: number;
22
- cost_usd: number;
23
- };
24
+ usage: RawUsage;
24
25
  }
25
26
  export interface EmbedRequest {
26
27
  model: string;
@@ -29,18 +30,15 @@ export interface EmbedRequest {
29
30
  export interface EmbedResponse {
30
31
  model: string;
31
32
  embeddings: number[][];
32
- usage: {
33
- input_tokens: number;
34
- cost_usd: number;
35
- };
33
+ usage: RawUsage;
36
34
  }
37
35
  export interface Model {
38
36
  id: string;
39
37
  kind: 'chat' | 'embed';
40
- context?: number;
38
+ context_window?: number;
41
39
  dimensions?: number;
42
- input_per_1m: number;
43
- output_per_1m?: number;
40
+ input_cost_per_1m_cents: number;
41
+ output_cost_per_1m_cents?: number;
44
42
  }
45
43
  export interface ModelsResponse {
46
44
  models: Model[];
@@ -48,3 +46,77 @@ export interface ModelsResponse {
48
46
  export declare function complete(apiKey: string, orgId: string, req: CompleteRequest): Promise<CompleteResponse>;
49
47
  export declare function embed(apiKey: string, orgId: string, req: EmbedRequest): Promise<EmbedResponse>;
50
48
  export declare function listModels(apiKey: string, orgId: string): Promise<ModelsResponse>;
49
+ export type Verb = 'classify' | 'extract' | 'summarize' | 'draft';
50
+ /** Routing hint; opaque — the server picks the model. With one served
51
+ * model today every tier resolves to it. `tier_used` echoes back what was
52
+ * actually selected. */
53
+ export type Tier = 'fast' | 'reasoning' | 'cheap';
54
+ export interface VerbUsage {
55
+ tier_used: Tier;
56
+ tokens_in: number;
57
+ tokens_out: number;
58
+ cost_cents: number;
59
+ }
60
+ interface VerbBase {
61
+ /** Routing hint. Defaults to 'fast'. */
62
+ tier?: Tier;
63
+ }
64
+ export interface ClassifyRequest extends VerbBase {
65
+ input: string;
66
+ /** Candidate labels — pick from this set. */
67
+ labels: string[];
68
+ /** When true, multiple labels may apply; response carries `labels[]`. */
69
+ multi?: boolean;
70
+ }
71
+ export interface ClassifyResponse {
72
+ data: {
73
+ label?: string;
74
+ labels?: string[];
75
+ };
76
+ usage: VerbUsage;
77
+ }
78
+ export interface ExtractRequest extends VerbBase {
79
+ input: string;
80
+ /** A JSON Schema (object) the extracted data must conform to. */
81
+ schema: Record<string, unknown>;
82
+ }
83
+ export interface ExtractResponse {
84
+ data: {
85
+ data: Record<string, unknown>;
86
+ };
87
+ usage: VerbUsage;
88
+ }
89
+ export interface SummarizeRequest extends VerbBase {
90
+ input: string;
91
+ /** `brief` (1-2 sentences), `exec` (3-4 decision-maker sentences), or
92
+ * `bullet` (short bulleted list). Default `brief`. */
93
+ style?: 'brief' | 'exec' | 'bullet';
94
+ }
95
+ export interface SummarizeResponse {
96
+ data: {
97
+ summary: string;
98
+ };
99
+ usage: VerbUsage;
100
+ }
101
+ export interface DraftRequest extends VerbBase {
102
+ /** Optional source content — may be empty when `context`/`prompt` carry
103
+ * the brief. */
104
+ input?: string;
105
+ /** What to write — `email`, `message`, `reply`, etc. */
106
+ kind: string;
107
+ /** Free-form structured context (recipient, tone, facts). */
108
+ context?: Record<string, unknown>;
109
+ /** Free-text instructions to the writer. */
110
+ prompt?: string;
111
+ }
112
+ export interface DraftResponse {
113
+ data: {
114
+ text: string;
115
+ };
116
+ usage: VerbUsage;
117
+ }
118
+ export declare function classify(apiKey: string, orgId: string, req: ClassifyRequest): Promise<ClassifyResponse>;
119
+ export declare function extract(apiKey: string, orgId: string, req: ExtractRequest): Promise<ExtractResponse>;
120
+ export declare function summarize(apiKey: string, orgId: string, req: SummarizeRequest): Promise<SummarizeResponse>;
121
+ export declare function draft(apiKey: string, orgId: string, req: DraftRequest): Promise<DraftResponse>;
122
+ export {};
package/dist/llm.js CHANGED
@@ -9,12 +9,17 @@ exports.EXPOSES = void 0;
9
9
  exports.complete = complete;
10
10
  exports.embed = embed;
11
11
  exports.listModels = listModels;
12
+ exports.classify = classify;
13
+ exports.extract = extract;
14
+ exports.summarize = summarize;
15
+ exports.draft = draft;
12
16
  const client_1 = require("./client");
13
17
  const config_1 = require("./config");
14
18
  exports.EXPOSES = [
15
19
  'POST /llm/orgs/{org_id}/complete',
16
20
  'POST /llm/orgs/{org_id}/embed',
17
21
  'GET /llm/orgs/{org_id}/models',
22
+ 'POST /llm/orgs/{org_id}/tasks/{verb}',
18
23
  ];
19
24
  async function complete(apiKey, orgId, req) {
20
25
  return (0, client_1.request)('POST', `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/complete`, apiKey, req);
@@ -25,3 +30,18 @@ async function embed(apiKey, orgId, req) {
25
30
  async function listModels(apiKey, orgId) {
26
31
  return (0, client_1.request)('GET', `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/models`, apiKey);
27
32
  }
33
+ function verbUrl(orgId, verb) {
34
+ return `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/tasks/${verb}`;
35
+ }
36
+ async function classify(apiKey, orgId, req) {
37
+ return (0, client_1.request)('POST', verbUrl(orgId, 'classify'), apiKey, req);
38
+ }
39
+ async function extract(apiKey, orgId, req) {
40
+ return (0, client_1.request)('POST', verbUrl(orgId, 'extract'), apiKey, req);
41
+ }
42
+ async function summarize(apiKey, orgId, req) {
43
+ return (0, client_1.request)('POST', verbUrl(orgId, 'summarize'), apiKey, req);
44
+ }
45
+ async function draft(apiKey, orgId, req) {
46
+ return (0, client_1.request)('POST', verbUrl(orgId, 'draft'), apiKey, req);
47
+ }
package/dist/workflow.js CHANGED
@@ -23,7 +23,33 @@ exports.EXPOSES = [
23
23
  'GET /workflow/orgs/{org_id}/workflows/{workflow_id}/runs',
24
24
  'GET /workflow/orgs/{org_id}/runs/{run_id}',
25
25
  ];
26
+ // Local URL-shape check on http_request steps. Same shape as the SSRF
27
+ // audit's scheme allowlist on webhook.forward_url and queue.consumer_url —
28
+ // this is *UX defense*, not security: the backend MUST also validate
29
+ // (which it does, via the same middleware) — see
30
+ // docs/cross-repo-prompts/backend-workflow-step-validation.md.
31
+ // Catches javascript:, file:, gopher:, data:, and whitespace/null-byte
32
+ // hosts at agent typo-fix latency instead of at "why didn't my workflow
33
+ // fire" investigation latency.
34
+ function validateSteps(steps) {
35
+ for (let i = 0; i < steps.length; i++) {
36
+ const s = steps[i];
37
+ if (s.type === 'http_request' || s.type === 'http') {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(s.url);
41
+ }
42
+ catch {
43
+ throw new Error(`step ${i} (${s.type}): "url" is not a valid URL — got ${JSON.stringify(s.url)}.`);
44
+ }
45
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
46
+ throw new Error(`step ${i} (${s.type}): "url" must use http:// or https:// scheme — got "${parsed.protocol}" in ${JSON.stringify(s.url)}.`);
47
+ }
48
+ }
49
+ }
50
+ }
26
51
  async function createWorkflow(apiKey, orgId, payload) {
52
+ validateSteps(payload.steps);
27
53
  return (0, client_1.request)('POST', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows`, apiKey, payload);
28
54
  }
29
55
  async function listWorkflows(apiKey, orgId) {
@@ -33,6 +59,8 @@ async function getWorkflow(apiKey, orgId, workflowId) {
33
59
  return (0, client_1.request)('GET', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey);
34
60
  }
35
61
  async function updateWorkflow(apiKey, orgId, workflowId, payload) {
62
+ if (payload.steps)
63
+ validateSteps(payload.steps);
36
64
  return (0, client_1.request)('PATCH', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey, payload);
37
65
  }
38
66
  async function enableWorkflow(apiKey, orgId, workflowId) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/sdk",
3
3
  "license": "Apache-2.0",
4
- "version": "1.3.6",
4
+ "version": "1.3.11",
5
5
  "description": "TypeScript SDK for the MyAPI ecosystem",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
package/src/email.ts CHANGED
@@ -137,6 +137,22 @@ export async function createMailbox(apiKey: string, domain: string, username: st
137
137
  return request('POST', `${BASE_URL}/email/mailboxes/create`, apiKey, { domain, username, display_name: displayName });
138
138
  }
139
139
 
140
+ // Backend (2026-06-06): idempotent — 204 whether the mailbox existed or not.
141
+ // Returns 409 MAILBOX_IN_USE when an active/paused campaign still sends from
142
+ // this address; pause/delete the campaign first.
143
+ export async function deleteMailbox(apiKey: string, address: string): Promise<void> {
144
+ return request('DELETE', `${BASE_URL}/email/mailboxes/${encodeURIComponent(address)}`, apiKey);
145
+ }
146
+
147
+ // Backend (2026-06-06): re-run Stalwart provisioning for a domain. Use when
148
+ // mailbox-create on a domain returns DOMAIN_NOT_MAIL_READY /
149
+ // MAILBOX_PROVISION_FAILED, or when the inbox endpoint returns
150
+ // "mail server error" on a domain whose `email_infra_ready` flag is stale.
151
+ // Idempotent.
152
+ export async function mailServerResync(apiKey: string, orgId: string, domain: string): Promise<{ ok: boolean } & Record<string, unknown>> {
153
+ return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/domains/${encodeURIComponent(domain)}/mail-server-resync`, apiKey);
154
+ }
155
+
140
156
  export async function listMailboxes(apiKey: string, options?: { domain?: string; filter?: 'unassigned' }): Promise<{ address: string; created_at: string }[]> {
141
157
  const query = new URLSearchParams();
142
158
  if (options?.domain) query.append('domain', options.domain);
package/src/funnel.ts CHANGED
@@ -12,6 +12,12 @@ export const EXPOSES: Exposes = [
12
12
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
13
13
  'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
14
14
  'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
15
+ // Backend (2026-06-03): canonical funnel-form proxy lands per-slug
16
+ // bindings — destination (webhook/workflow), fields, honeypot, rate
17
+ // limit. See docs/cross-repo-prompts/cli-funnel-form-canonical-followup-2026-06-04.md.
18
+ 'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
19
+ 'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
20
+ 'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}/forms/{slug}',
15
21
  ];
16
22
 
17
23
  export interface Funnel {
@@ -21,10 +27,37 @@ export interface Funnel {
21
27
  // to the org's preview_subdomain for back-compat. Get / list responses
22
28
  // include it when set.
23
29
  name?: string;
30
+ // Backend (2026-06-03): every funnel auto-provisions a webhook_endpoints
31
+ // row at create. Submissions to `/submit/{slug}` without a per-slug
32
+ // binding fall through to this webhook. Optional only because legacy
33
+ // funnels migrated by the boot job may be briefly null.
34
+ org_webhook_id?: string;
24
35
  created_at: string;
25
36
  updated_at: string;
26
37
  }
38
+
39
+ // Per-slug binding from `POST /funnel/.../forms`. Backend resolves a
40
+ // submission's destination at submit time: per-slug binding wins, then
41
+ // `Funnel.org_webhook_id`, else `NO_DESTINATION`.
42
+ export interface FormFieldSpec {
43
+ name: string;
44
+ required?: boolean;
45
+ oneof?: string[];
46
+ max_length?: number;
47
+ regex?: string;
48
+ }
49
+ export interface FormBinding {
50
+ slug: string;
51
+ // "<kind>:<uuid>" — kind is "webhook" or "workflow".
52
+ destination: string;
53
+ fields?: FormFieldSpec[];
54
+ honeypot_field?: string;
55
+ rate_limit?: { per_minute: number };
56
+ }
27
57
  export interface CreateFunnelResponse { funnel: Funnel; subdomain_url?: string; domain_url?: string }
58
+ // Same envelope shape on read — kept as its own name so call sites read
59
+ // honestly. Wire shape is `{funnel: {...}, subdomain_url?, domain_url?}`.
60
+ export interface GetFunnelResponse { funnel: Funnel; subdomain_url?: string; domain_url?: string }
28
61
  export interface VerifyResult {
29
62
  pages?: Array<{ slug: string; tests: VerifyTest[] }>;
30
63
  tests?: VerifyTest[];
@@ -36,8 +69,19 @@ export async function createFunnel(apiKey: string, orgId: string, opts?: { name?
36
69
  // matching the reserved-suffix rules; agent should pre-validate.
37
70
  return request('POST', `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels`, apiKey, opts?.name ? { name: opts.name } : {});
38
71
  }
39
- export async function getFunnel(apiKey: string, orgId: string, funnelId: string): Promise<Funnel> {
40
- return request('GET', `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}`, apiKey);
72
+ export async function getFunnel(apiKey: string, orgId: string, funnelId: string): Promise<GetFunnelResponse> {
73
+ // Backend has wrapped GET responses since 2026-04 (`{funnel, subdomain_url, domain_url}`).
74
+ // Older builds returned a flat Funnel — normalize to the envelope so
75
+ // callers can rely on the typed shape.
76
+ const raw = await request<GetFunnelResponse | Funnel>(
77
+ 'GET',
78
+ `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}`,
79
+ apiKey,
80
+ );
81
+ if (raw && typeof raw === 'object' && 'funnel' in raw && (raw as GetFunnelResponse).funnel) {
82
+ return raw as GetFunnelResponse;
83
+ }
84
+ return { funnel: raw as Funnel };
41
85
  }
42
86
  export async function listFunnels(apiKey: string, orgId: string): Promise<Funnel[]> {
43
87
  return request('GET', `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels`, apiKey);
@@ -138,3 +182,22 @@ export async function publishFiles(
138
182
  }
139
183
  return (result as ApiResponse<PublishResult>).data as PublishResult;
140
184
  }
185
+
186
+ // ── Form bindings (canonical funnel-form proxy) ────────────────────────────
187
+
188
+ function formsBase(orgId: string, funnelId: string): string {
189
+ return `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/forms`;
190
+ }
191
+
192
+ export async function createFormBinding(apiKey: string, orgId: string, funnelId: string, binding: FormBinding): Promise<FormBinding> {
193
+ return request('POST', formsBase(orgId, funnelId), apiKey, binding);
194
+ }
195
+
196
+ export async function listFormBindings(apiKey: string, orgId: string, funnelId: string): Promise<FormBinding[]> {
197
+ const res = await request<FormBinding[] | { forms?: FormBinding[] }>('GET', formsBase(orgId, funnelId), apiKey);
198
+ return Array.isArray(res) ? res : (res?.forms ?? []);
199
+ }
200
+
201
+ export async function deleteFormBinding(apiKey: string, orgId: string, funnelId: string, slug: string): Promise<void> {
202
+ return request('DELETE', `${formsBase(orgId, funnelId)}/${encodeURIComponent(slug)}`, apiKey);
203
+ }
package/src/hq.ts CHANGED
@@ -14,6 +14,11 @@ export const EXPOSES: Exposes = [
14
14
  'POST /hq/account/keys/revoke-all',
15
15
  'DELETE /hq/account/delete/key/{key_id}',
16
16
  'PATCH /hq/account/spend-cap',
17
+ // Backend (2026-06-06): account-scoped mailing address — required for
18
+ // transactional email per CAN-SPAM. PATCH sets, GET reads (null when
19
+ // unset). Hard-gates POST /email/send with MAILING_ADDRESS_REQUIRED.
20
+ 'PATCH /hq/account/mailing-address',
21
+ 'GET /hq/account/mailing-address',
17
22
  'POST /hq/orgs',
18
23
  'GET /hq/orgs',
19
24
  'GET /hq/orgs/{org_id}',
@@ -148,6 +153,16 @@ export async function getAccount(apiKey: string): Promise<AccountInfo> {
148
153
  return request('GET', `${BASE_URL}/hq/account/me`, apiKey);
149
154
  }
150
155
 
156
+ // Mailing address — required for transactional `email send` (CAN-SPAM).
157
+ // `getMailingAddress` returns `null` for `mailing_address` when unset.
158
+ export async function getMailingAddress(apiKey: string): Promise<{ mailing_address: string | null }> {
159
+ return request('GET', `${BASE_URL}/hq/account/mailing-address`, apiKey);
160
+ }
161
+
162
+ export async function setMailingAddress(apiKey: string, mailingAddress: string): Promise<{ mailing_address: string }> {
163
+ return request('PATCH', `${BASE_URL}/hq/account/mailing-address`, apiKey, { mailing_address: mailingAddress });
164
+ }
165
+
151
166
  export interface CreateApiKeyOptions {
152
167
  // Slot grants. Omitted → backend defaults to unrestricted ({"*":"write"}).
153
168
  grants?: Grants;
package/src/llm.ts CHANGED
@@ -12,8 +12,15 @@ export const EXPOSES: Exposes = [
12
12
  'POST /llm/orgs/{org_id}/complete',
13
13
  'POST /llm/orgs/{org_id}/embed',
14
14
  'GET /llm/orgs/{org_id}/models',
15
+ 'POST /llm/orgs/{org_id}/tasks/{verb}',
15
16
  ];
16
17
 
18
+ // ── Raw surface ──────────────────────────────────────────────────────────
19
+ // Backed *exclusively* by self-hosted open-source models (Qwen on our TPU).
20
+ // The customer picks the model id from the catalog. Proprietary models are
21
+ // not callable here — that rule is what keeps the LLM slot off the
22
+ // reseller framing.
23
+
17
24
  export type Role = 'system' | 'user' | 'assistant';
18
25
 
19
26
  export interface Message {
@@ -29,15 +36,17 @@ export interface CompleteRequest {
29
36
  stop?: string[];
30
37
  }
31
38
 
39
+ export interface RawUsage {
40
+ input_tokens: number;
41
+ output_tokens?: number;
42
+ cost_cents: number;
43
+ }
44
+
32
45
  export interface CompleteResponse {
33
46
  model: string;
34
47
  content: string;
35
- finish_reason: string; // 'stop' | 'length' | 'safety' | ...
36
- usage: {
37
- input_tokens: number;
38
- output_tokens: number;
39
- cost_usd: number;
40
- };
48
+ finish_reason: string; // 'stop' | 'length' | 'filter'
49
+ usage: RawUsage;
41
50
  }
42
51
 
43
52
  export interface EmbedRequest {
@@ -47,26 +56,22 @@ export interface EmbedRequest {
47
56
 
48
57
  // Always returned as an array — even for a single-string input you get a
49
58
  // one-element array. Each element is the dense vector as a flat number[].
50
- // Embed-only usage has no output_tokens.
51
59
  export interface EmbedResponse {
52
60
  model: string;
53
61
  embeddings: number[][];
54
- usage: {
55
- input_tokens: number;
56
- cost_usd: number;
57
- };
62
+ usage: RawUsage;
58
63
  }
59
64
 
60
- // Catalog row. 'chat' models have context + output_per_1m; 'embed' models
61
- // have dimensions instead. Pricing is per 1M tokens at upstream rates
62
- // (we resell at-cost while building our own inference).
65
+ // Catalog row. `chat` models have context_window + output_cost_per_1m_cents;
66
+ // `embed` models have dimensions instead. Pricing is in cents per 1M tokens
67
+ // (the platform's native unit cost_cents everywhere).
63
68
  export interface Model {
64
69
  id: string;
65
70
  kind: 'chat' | 'embed';
66
- context?: number;
71
+ context_window?: number;
67
72
  dimensions?: number;
68
- input_per_1m: number;
69
- output_per_1m?: number;
73
+ input_cost_per_1m_cents: number;
74
+ output_cost_per_1m_cents?: number;
70
75
  }
71
76
 
72
77
  export interface ModelsResponse {
@@ -84,3 +89,101 @@ export async function embed(apiKey: string, orgId: string, req: EmbedRequest): P
84
89
  export async function listModels(apiKey: string, orgId: string): Promise<ModelsResponse> {
85
90
  return request('GET', `${BASE_URL}/llm/orgs/${encodeURIComponent(orgId)}/models`, apiKey);
86
91
  }
92
+
93
+ // ── Verb surface — objective-based completion ────────────────────────────
94
+ // The customer asks for a task done (classify / extract / summarize /
95
+ // draft); the model is implementation detail and is never named in the
96
+ // response. This is where any future proprietary model lives — wrapped, not
97
+ // resold.
98
+
99
+ export type Verb = 'classify' | 'extract' | 'summarize' | 'draft';
100
+
101
+ /** Routing hint; opaque — the server picks the model. With one served
102
+ * model today every tier resolves to it. `tier_used` echoes back what was
103
+ * actually selected. */
104
+ export type Tier = 'fast' | 'reasoning' | 'cheap';
105
+
106
+ export interface VerbUsage {
107
+ tier_used: Tier;
108
+ tokens_in: number;
109
+ tokens_out: number;
110
+ cost_cents: number;
111
+ }
112
+
113
+ interface VerbBase {
114
+ /** Routing hint. Defaults to 'fast'. */
115
+ tier?: Tier;
116
+ }
117
+
118
+ export interface ClassifyRequest extends VerbBase {
119
+ input: string;
120
+ /** Candidate labels — pick from this set. */
121
+ labels: string[];
122
+ /** When true, multiple labels may apply; response carries `labels[]`. */
123
+ multi?: boolean;
124
+ }
125
+
126
+ export interface ClassifyResponse {
127
+ data: { label?: string; labels?: string[] };
128
+ usage: VerbUsage;
129
+ }
130
+
131
+ export interface ExtractRequest extends VerbBase {
132
+ input: string;
133
+ /** A JSON Schema (object) the extracted data must conform to. */
134
+ schema: Record<string, unknown>;
135
+ }
136
+
137
+ export interface ExtractResponse {
138
+ data: { data: Record<string, unknown> };
139
+ usage: VerbUsage;
140
+ }
141
+
142
+ export interface SummarizeRequest extends VerbBase {
143
+ input: string;
144
+ /** `brief` (1-2 sentences), `exec` (3-4 decision-maker sentences), or
145
+ * `bullet` (short bulleted list). Default `brief`. */
146
+ style?: 'brief' | 'exec' | 'bullet';
147
+ }
148
+
149
+ export interface SummarizeResponse {
150
+ data: { summary: string };
151
+ usage: VerbUsage;
152
+ }
153
+
154
+ export interface DraftRequest extends VerbBase {
155
+ /** Optional source content — may be empty when `context`/`prompt` carry
156
+ * the brief. */
157
+ input?: string;
158
+ /** What to write — `email`, `message`, `reply`, etc. */
159
+ kind: string;
160
+ /** Free-form structured context (recipient, tone, facts). */
161
+ context?: Record<string, unknown>;
162
+ /** Free-text instructions to the writer. */
163
+ prompt?: string;
164
+ }
165
+
166
+ export interface DraftResponse {
167
+ data: { text: string };
168
+ usage: VerbUsage;
169
+ }
170
+
171
+ function verbUrl(orgId: string, verb: Verb): string {
172
+ return `${BASE_URL}/llm/orgs/${encodeURIComponent(orgId)}/tasks/${verb}`;
173
+ }
174
+
175
+ export async function classify(apiKey: string, orgId: string, req: ClassifyRequest): Promise<ClassifyResponse> {
176
+ return request('POST', verbUrl(orgId, 'classify'), apiKey, req);
177
+ }
178
+
179
+ export async function extract(apiKey: string, orgId: string, req: ExtractRequest): Promise<ExtractResponse> {
180
+ return request('POST', verbUrl(orgId, 'extract'), apiKey, req);
181
+ }
182
+
183
+ export async function summarize(apiKey: string, orgId: string, req: SummarizeRequest): Promise<SummarizeResponse> {
184
+ return request('POST', verbUrl(orgId, 'summarize'), apiKey, req);
185
+ }
186
+
187
+ export async function draft(apiKey: string, orgId: string, req: DraftRequest): Promise<DraftResponse> {
188
+ return request('POST', verbUrl(orgId, 'draft'), apiKey, req);
189
+ }
package/src/workflow.ts CHANGED
@@ -64,11 +64,36 @@ export interface WorkflowRun {
64
64
  created_at: string;
65
65
  }
66
66
 
67
+ // Local URL-shape check on http_request steps. Same shape as the SSRF
68
+ // audit's scheme allowlist on webhook.forward_url and queue.consumer_url —
69
+ // this is *UX defense*, not security: the backend MUST also validate
70
+ // (which it does, via the same middleware) — see
71
+ // docs/cross-repo-prompts/backend-workflow-step-validation.md.
72
+ // Catches javascript:, file:, gopher:, data:, and whitespace/null-byte
73
+ // hosts at agent typo-fix latency instead of at "why didn't my workflow
74
+ // fire" investigation latency.
75
+ function validateSteps(steps: WorkflowStep[]): void {
76
+ for (let i = 0; i < steps.length; i++) {
77
+ const s = steps[i];
78
+ if (s.type === 'http_request' || s.type === 'http') {
79
+ let parsed: URL;
80
+ try { parsed = new URL(s.url); }
81
+ catch {
82
+ throw new Error(`step ${i} (${s.type}): "url" is not a valid URL — got ${JSON.stringify(s.url)}.`);
83
+ }
84
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
85
+ throw new Error(`step ${i} (${s.type}): "url" must use http:// or https:// scheme — got "${parsed.protocol}" in ${JSON.stringify(s.url)}.`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
67
91
  export async function createWorkflow(apiKey: string, orgId: string, payload: {
68
92
  name: string;
69
93
  trigger_config: { endpoint_id: string };
70
94
  steps: WorkflowStep[];
71
95
  }): Promise<Workflow> {
96
+ validateSteps(payload.steps);
72
97
  return request('POST', `${BASE_URL}/workflow/orgs/${encodeURIComponent(orgId)}/workflows`, apiKey, payload);
73
98
  }
74
99
 
@@ -81,6 +106,7 @@ export async function getWorkflow(apiKey: string, orgId: string, workflowId: str
81
106
  }
82
107
 
83
108
  export async function updateWorkflow(apiKey: string, orgId: string, workflowId: string, payload: { name?: string; trigger_config?: { endpoint_id: string }; steps?: WorkflowStep[] }): Promise<Workflow> {
109
+ if (payload.steps) validateSteps(payload.steps);
84
110
  return request('PATCH', `${BASE_URL}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey, payload);
85
111
  }
86
112