@myapihq/cli 1.2.2 → 1.2.5
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/commands/domain.d.ts +2 -0
- package/dist/commands/domain.js +106 -9
- package/dist/commands/fn-validation.test.d.ts +1 -0
- package/dist/commands/fn-validation.test.js +60 -0
- package/dist/commands/fn.d.ts +13 -0
- package/dist/commands/fn.js +180 -0
- package/dist/commands/funnel.js +18 -5
- package/dist/commands/webhook-validation.test.d.ts +1 -0
- package/dist/commands/webhook-validation.test.js +35 -0
- package/dist/commands/webhook.d.ts +2 -0
- package/dist/commands/webhook.js +72 -2
- package/dist/commands/workflow-validation.test.d.ts +1 -0
- package/dist/commands/workflow-validation.test.js +137 -0
- package/dist/commands/workflow.d.ts +1 -0
- package/dist/commands/workflow.js +58 -17
- package/dist/exposes.test.js +1 -0
- package/dist/index.js +24 -4
- package/dist/sdk-domain-assign.test.d.ts +1 -0
- package/dist/sdk-domain-assign.test.js +53 -0
- package/dist/sdk-function.test.d.ts +1 -0
- package/dist/sdk-function.test.js +190 -0
- package/dist/sdk-funnel-name.test.d.ts +1 -0
- package/dist/sdk-funnel-name.test.js +48 -0
- package/dist/sdk-webhook.test.d.ts +1 -0
- package/dist/sdk-webhook.test.js +86 -0
- package/dist/skills/my-domain-api/SKILL.md +10 -0
- package/package.json +2 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SDK-level unit tests for the function module. Verifies request URL/body
|
|
2
|
+
// shape, response parsing, and error envelopes against the Story 1 contract
|
|
3
|
+
// shipped by myapi-hq/internal/routes/function/crud.go.
|
|
4
|
+
//
|
|
5
|
+
// Mocks global fetch — does NOT hit the network.
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { fn, MyApiError } from '@myapihq/sdk';
|
|
8
|
+
const API_KEY = 'myapi_test_abc';
|
|
9
|
+
const ORG_ID = '11111111-1111-4111-8111-111111111111';
|
|
10
|
+
const FN_ID = '22222222-2222-4222-8222-222222222222';
|
|
11
|
+
let fetchMock;
|
|
12
|
+
function ok(data, status = 200) {
|
|
13
|
+
// Backend wraps everything in `{ success, data, meta }`. The SDK unwraps.
|
|
14
|
+
return new Response(JSON.stringify({ success: true, data, meta: {} }), {
|
|
15
|
+
status,
|
|
16
|
+
headers: { 'content-type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function fail(code, message, status = 422) {
|
|
20
|
+
return new Response(JSON.stringify({ success: false, error: { code, message }, meta: {} }), {
|
|
21
|
+
status,
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
fetchMock = vi.fn();
|
|
27
|
+
globalThis.fetch = fetchMock;
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
describe('fn.createFunction', () => {
|
|
33
|
+
it('POSTs to /function/orgs/{org_id}/functions with the bearer auth + JSON body', async () => {
|
|
34
|
+
fetchMock.mockResolvedValueOnce(ok({
|
|
35
|
+
function: { id: FN_ID, org_id: ORG_ID, name: 'my-app-api', trigger_type: 'http', invocation_url: '', created_at: 't1', updated_at: 't1' },
|
|
36
|
+
scoped_api_key: 'myapi_live_scoped_xyz',
|
|
37
|
+
scoped_api_key_id: 'key_xyz',
|
|
38
|
+
}));
|
|
39
|
+
const result = await fn.createFunction(API_KEY, ORG_ID, { name: 'my-app-api', trigger_type: 'http' });
|
|
40
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
41
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
42
|
+
expect(url).toContain(`/function/orgs/${ORG_ID}/functions`);
|
|
43
|
+
expect(init.method).toBe('POST');
|
|
44
|
+
expect(init.headers.Authorization).toBe(`Bearer ${API_KEY}`);
|
|
45
|
+
expect(init.headers['Content-Type']).toBe('application/json');
|
|
46
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'my-app-api', trigger_type: 'http' });
|
|
47
|
+
expect(result.function.id).toBe(FN_ID);
|
|
48
|
+
expect(result.scoped_api_key).toBe('myapi_live_scoped_xyz');
|
|
49
|
+
expect(result.scoped_api_key_id).toBe('key_xyz');
|
|
50
|
+
// Invocation URL is empty in Story 1 — populated by Story 2.
|
|
51
|
+
expect(result.function.invocation_url).toBe('');
|
|
52
|
+
});
|
|
53
|
+
it('passes cron_schedule through when trigger_type=cron', async () => {
|
|
54
|
+
fetchMock.mockResolvedValueOnce(ok({
|
|
55
|
+
function: { id: FN_ID, org_id: ORG_ID, name: 'daily-report', trigger_type: 'cron', cron_schedule: '0 8 * * *', invocation_url: '', created_at: 't1', updated_at: 't1' },
|
|
56
|
+
scoped_api_key: 'myapi_live_scoped_xyz',
|
|
57
|
+
scoped_api_key_id: 'key_xyz',
|
|
58
|
+
}));
|
|
59
|
+
await fn.createFunction(API_KEY, ORG_ID, { name: 'daily-report', trigger_type: 'cron', cron_schedule: '0 8 * * *' });
|
|
60
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
61
|
+
expect(body).toEqual({ name: 'daily-report', trigger_type: 'cron', cron_schedule: '0 8 * * *' });
|
|
62
|
+
});
|
|
63
|
+
it('encodes path params (defensive against : ? / # in org id)', async () => {
|
|
64
|
+
// UUIDs don't contain reserved chars in practice — but the SDK still
|
|
65
|
+
// encodes, so an alternate id with unsafe chars wouldn't break path
|
|
66
|
+
// routing.
|
|
67
|
+
fetchMock.mockResolvedValueOnce(ok({
|
|
68
|
+
function: { id: FN_ID, org_id: 'org/with?weird#chars', name: 'x', trigger_type: 'http', invocation_url: '', created_at: 't', updated_at: 't' },
|
|
69
|
+
scoped_api_key: 'k', scoped_api_key_id: 'i',
|
|
70
|
+
}));
|
|
71
|
+
await fn.createFunction(API_KEY, 'org/with?weird#chars', { name: 'x' });
|
|
72
|
+
const [url] = fetchMock.mock.calls[0];
|
|
73
|
+
expect(url).toContain('/function/orgs/org%2Fwith%3Fweird%23chars/functions');
|
|
74
|
+
});
|
|
75
|
+
it('surfaces backend INVALID_NAME (422) as a typed MyApiError', async () => {
|
|
76
|
+
fetchMock.mockResolvedValueOnce(fail('INVALID_NAME', 'name must match ^[a-z0-9][a-z0-9-]{0,49}$', 422));
|
|
77
|
+
await expect(fn.createFunction(API_KEY, ORG_ID, { name: 'BAD NAME' }))
|
|
78
|
+
.rejects.toMatchObject({
|
|
79
|
+
name: 'MyApiError',
|
|
80
|
+
code: 'INVALID_NAME',
|
|
81
|
+
status: 422,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('surfaces NAME_TAKEN (409) cleanly', async () => {
|
|
85
|
+
fetchMock.mockResolvedValueOnce(fail('NAME_TAKEN', 'function name "x" is already used in this org', 409));
|
|
86
|
+
await expect(fn.createFunction(API_KEY, ORG_ID, { name: 'x' }))
|
|
87
|
+
.rejects.toMatchObject({ code: 'NAME_TAKEN', status: 409 });
|
|
88
|
+
});
|
|
89
|
+
it('surfaces 401 (invalid api key)', async () => {
|
|
90
|
+
fetchMock.mockResolvedValueOnce(fail('unauthorized', 'invalid api key', 401));
|
|
91
|
+
await expect(fn.createFunction(API_KEY, ORG_ID, { name: 'x' }))
|
|
92
|
+
.rejects.toMatchObject({ status: 401 });
|
|
93
|
+
});
|
|
94
|
+
it('surfaces 403 SCOPE_FORBIDDEN when called with a slot_call-only key', async () => {
|
|
95
|
+
// 2026-05-15 enforcement: scoped api_keys with scopes=["slot_call"]
|
|
96
|
+
// can only call slot endpoints, not /hq/*, /admin/*, /internal/*.
|
|
97
|
+
// Note: /function/* IS a slot path, so SCOPE_FORBIDDEN shouldn't trigger
|
|
98
|
+
// here in practice — but the SDK must still surface it cleanly if it
|
|
99
|
+
// ever appears (e.g. for /hq/* mistakes).
|
|
100
|
+
fetchMock.mockResolvedValueOnce(fail('SCOPE_FORBIDDEN', 'this key cannot call /hq/* endpoints', 403));
|
|
101
|
+
await expect(fn.createFunction(API_KEY, ORG_ID, { name: 'x' }))
|
|
102
|
+
.rejects.toMatchObject({ code: 'SCOPE_FORBIDDEN', status: 403 });
|
|
103
|
+
});
|
|
104
|
+
it('wraps non-JSON 5xx responses (HTML error pages) helpfully', async () => {
|
|
105
|
+
fetchMock.mockResolvedValueOnce(new Response('<html>502 Bad Gateway</html>', {
|
|
106
|
+
status: 502, headers: { 'content-type': 'text/html' },
|
|
107
|
+
}));
|
|
108
|
+
await expect(fn.createFunction(API_KEY, ORG_ID, { name: 'x' }))
|
|
109
|
+
.rejects.toMatchObject({ code: 'invalid_json_response', status: 502 });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('fn.listFunctions', () => {
|
|
113
|
+
it('GETs /function/orgs/{org_id}/functions with bearer auth + no body', async () => {
|
|
114
|
+
fetchMock.mockResolvedValueOnce(ok([
|
|
115
|
+
{ id: FN_ID, org_id: ORG_ID, name: 'a', trigger_type: 'http', invocation_url: '', created_at: 't', updated_at: 't' },
|
|
116
|
+
]));
|
|
117
|
+
const fns = await fn.listFunctions(API_KEY, ORG_ID);
|
|
118
|
+
expect(fns).toHaveLength(1);
|
|
119
|
+
expect(fns[0].name).toBe('a');
|
|
120
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
121
|
+
expect(url).toContain(`/function/orgs/${ORG_ID}/functions`);
|
|
122
|
+
expect(init.method).toBe('GET');
|
|
123
|
+
expect(init.body).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
it('returns an empty array when org has no functions', async () => {
|
|
126
|
+
fetchMock.mockResolvedValueOnce(ok([]));
|
|
127
|
+
expect(await fn.listFunctions(API_KEY, ORG_ID)).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('fn.getFunction', () => {
|
|
131
|
+
it('GETs /function/orgs/{org_id}/functions/{id}', async () => {
|
|
132
|
+
fetchMock.mockResolvedValueOnce(ok({ id: FN_ID, org_id: ORG_ID, name: 'a', trigger_type: 'http', invocation_url: '', created_at: 't', updated_at: 't' }));
|
|
133
|
+
const got = await fn.getFunction(API_KEY, ORG_ID, FN_ID);
|
|
134
|
+
expect(got.id).toBe(FN_ID);
|
|
135
|
+
const [url] = fetchMock.mock.calls[0];
|
|
136
|
+
expect(url).toContain(`/function/orgs/${ORG_ID}/functions/${FN_ID}`);
|
|
137
|
+
});
|
|
138
|
+
it('throws function_not_found on 404', async () => {
|
|
139
|
+
fetchMock.mockResolvedValueOnce(fail('function_not_found', 'not found', 404));
|
|
140
|
+
await expect(fn.getFunction(API_KEY, ORG_ID, FN_ID))
|
|
141
|
+
.rejects.toMatchObject({ code: 'function_not_found', status: 404 });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('fn.deleteFunction', () => {
|
|
145
|
+
it('DELETEs /function/orgs/{org_id}/functions/{id}', async () => {
|
|
146
|
+
fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
147
|
+
await fn.deleteFunction(API_KEY, ORG_ID, FN_ID);
|
|
148
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
149
|
+
expect(url).toContain(`/function/orgs/${ORG_ID}/functions/${FN_ID}`);
|
|
150
|
+
expect(init.method).toBe('DELETE');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('fn.EXPOSES', () => {
|
|
154
|
+
it('matches the Story 1 contract — exactly 4 endpoints', () => {
|
|
155
|
+
expect(fn.EXPOSES).toEqual([
|
|
156
|
+
'POST /function/orgs/{org_id}/functions',
|
|
157
|
+
'GET /function/orgs/{org_id}/functions',
|
|
158
|
+
'GET /function/orgs/{org_id}/functions/{id}',
|
|
159
|
+
'DELETE /function/orgs/{org_id}/functions/{id}',
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
it('does NOT yet expose /logs or /env (Story 4/5 pending)', () => {
|
|
163
|
+
const flat = fn.EXPOSES.join(' ');
|
|
164
|
+
expect(flat).not.toContain('/logs');
|
|
165
|
+
expect(flat).not.toContain('/env');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
// MyApiError is the universal error shape callers can `instanceof`-check or
|
|
169
|
+
// switch on `.code`. Validate it surfaces the right fields from typed
|
|
170
|
+
// (object) errors and raw-string errors alike.
|
|
171
|
+
describe('MyApiError surfacing', () => {
|
|
172
|
+
it('preserves the body object on object-shaped errors (for CF_API_ERROR etc.)', async () => {
|
|
173
|
+
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
174
|
+
success: false,
|
|
175
|
+
error: { code: 'CF_API_ERROR', message: 'cf failed', cf_message: 'Forbidden zone', cf_status: 403 },
|
|
176
|
+
meta: {},
|
|
177
|
+
}), { status: 500, headers: { 'content-type': 'application/json' } }));
|
|
178
|
+
try {
|
|
179
|
+
await fn.createFunction(API_KEY, ORG_ID, { name: 'x' });
|
|
180
|
+
expect.fail('Should have thrown');
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
expect(e).toBeInstanceOf(MyApiError);
|
|
184
|
+
const err = e;
|
|
185
|
+
expect(err.code).toBe('CF_API_ERROR');
|
|
186
|
+
expect(err.body?.cf_message).toBe('Forbidden zone');
|
|
187
|
+
expect(err.body?.cf_status).toBe(403);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 2026-05-15: POST /funnel/orgs/{org_id}/funnels now accepts optional
|
|
2
|
+
// `name` (defaults server-side to the org's preview_subdomain). SDK
|
|
3
|
+
// createFunnel was extended to forward { name } through.
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { funnel } from '@myapihq/sdk';
|
|
6
|
+
const API_KEY = 'myapi_test';
|
|
7
|
+
const ORG_ID = '11111111-1111-4111-8111-111111111111';
|
|
8
|
+
let fetchMock;
|
|
9
|
+
function ok(data) {
|
|
10
|
+
return new Response(JSON.stringify({ success: true, data, meta: {} }), {
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: { 'content-type': 'application/json' },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetchMock = vi.fn().mockResolvedValue(ok({
|
|
17
|
+
funnel: { id: 'f1', org_id: ORG_ID, name: 'my-app', created_at: 't', updated_at: 't' },
|
|
18
|
+
subdomain_url: 'https://my-app.makeautonomous.com',
|
|
19
|
+
}));
|
|
20
|
+
globalThis.fetch = fetchMock;
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
23
|
+
describe('funnel.createFunnel — name (additive 2026-05-15)', () => {
|
|
24
|
+
it('omits name from body when not provided (back-compat — server picks preview_subdomain)', async () => {
|
|
25
|
+
await funnel.createFunnel(API_KEY, ORG_ID);
|
|
26
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
it('omits name when opts is provided but has no name', async () => {
|
|
29
|
+
await funnel.createFunnel(API_KEY, ORG_ID, {});
|
|
30
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
it('includes name when provided', async () => {
|
|
33
|
+
await funnel.createFunnel(API_KEY, ORG_ID, { name: 'my-app' });
|
|
34
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ name: 'my-app' });
|
|
35
|
+
});
|
|
36
|
+
it('hits the right path and method', async () => {
|
|
37
|
+
await funnel.createFunnel(API_KEY, ORG_ID, { name: 'x' });
|
|
38
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
39
|
+
expect(url).toContain(`/funnel/orgs/${ORG_ID}/funnels`);
|
|
40
|
+
expect(init.method).toBe('POST');
|
|
41
|
+
expect(init.headers.Authorization).toBe(`Bearer ${API_KEY}`);
|
|
42
|
+
});
|
|
43
|
+
it('returns the funnel with name populated', async () => {
|
|
44
|
+
const result = await funnel.createFunnel(API_KEY, ORG_ID, { name: 'my-app' });
|
|
45
|
+
expect(result.funnel.name).toBe('my-app');
|
|
46
|
+
expect(result.subdomain_url).toBe('https://my-app.makeautonomous.com');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// 2026-05-15 additive changes for my-webhook-api:
|
|
2
|
+
// - POST /webhook/orgs/{org_id}/endpoints body now accepts forward_url.
|
|
3
|
+
// - PATCH /webhook/orgs/{org_id}/endpoints/{id} is a new endpoint.
|
|
4
|
+
//
|
|
5
|
+
// Both surfaced through the SDK (createEndpoint+forward_url, updateEndpoint).
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { webhook } from '@myapihq/sdk';
|
|
8
|
+
const API_KEY = 'myapi_test';
|
|
9
|
+
const ORG_ID = '11111111-1111-4111-8111-111111111111';
|
|
10
|
+
const ENDPOINT_ID = '22222222-2222-4222-8222-222222222222';
|
|
11
|
+
let fetchMock;
|
|
12
|
+
function ok(data) {
|
|
13
|
+
return new Response(JSON.stringify({ success: true, data, meta: {} }), {
|
|
14
|
+
status: 200,
|
|
15
|
+
headers: { 'content-type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
fetchMock = vi.fn().mockResolvedValue(ok({
|
|
20
|
+
id: ENDPOINT_ID, org_id: ORG_ID, name: 'x', slug: 'abc', url: 'https://webhook/in/abc',
|
|
21
|
+
}));
|
|
22
|
+
globalThis.fetch = fetchMock;
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
describe('webhook.createEndpoint — forward_url (additive 2026-05-15)', () => {
|
|
28
|
+
it('omits forward_url by default (back-compat)', async () => {
|
|
29
|
+
await webhook.createEndpoint(API_KEY, ORG_ID, 'my-hook');
|
|
30
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
31
|
+
expect(body).toEqual({ name: 'my-hook' });
|
|
32
|
+
expect(body).not.toHaveProperty('forward_url');
|
|
33
|
+
});
|
|
34
|
+
it('includes forward_url when passed', async () => {
|
|
35
|
+
await webhook.createEndpoint(API_KEY, ORG_ID, 'my-hook', {
|
|
36
|
+
forward_url: 'https://example.com/forward',
|
|
37
|
+
});
|
|
38
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
39
|
+
expect(body.forward_url).toBe('https://example.com/forward');
|
|
40
|
+
});
|
|
41
|
+
it('allows explicit empty string for forward_url (clears it)', async () => {
|
|
42
|
+
await webhook.createEndpoint(API_KEY, ORG_ID, 'my-hook', { forward_url: '' });
|
|
43
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
44
|
+
expect(body).toHaveProperty('forward_url', '');
|
|
45
|
+
});
|
|
46
|
+
it('combines forward_url with other options', async () => {
|
|
47
|
+
await webhook.createEndpoint(API_KEY, ORG_ID, 'my-hook', {
|
|
48
|
+
description: 'Stripe events',
|
|
49
|
+
crm_email_path: 'data.object.customer_email',
|
|
50
|
+
forward_url: 'https://example.com/forward',
|
|
51
|
+
});
|
|
52
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
53
|
+
expect(body).toEqual({
|
|
54
|
+
name: 'my-hook',
|
|
55
|
+
description: 'Stripe events',
|
|
56
|
+
crm_email_path: 'data.object.customer_email',
|
|
57
|
+
forward_url: 'https://example.com/forward',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('webhook.updateEndpoint — PATCH (new 2026-05-15)', () => {
|
|
62
|
+
it('PATCHes /webhook/orgs/{org_id}/endpoints/{id}', async () => {
|
|
63
|
+
await webhook.updateEndpoint(API_KEY, ORG_ID, ENDPOINT_ID, { name: 'renamed' });
|
|
64
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
65
|
+
expect(url).toContain(`/webhook/orgs/${ORG_ID}/endpoints/${ENDPOINT_ID}`);
|
|
66
|
+
expect(init.method).toBe('PATCH');
|
|
67
|
+
expect(JSON.parse(init.body)).toEqual({ name: 'renamed' });
|
|
68
|
+
});
|
|
69
|
+
it('sends only the fields that were provided', async () => {
|
|
70
|
+
await webhook.updateEndpoint(API_KEY, ORG_ID, ENDPOINT_ID, {
|
|
71
|
+
forward_url: 'https://other.example.com/fwd',
|
|
72
|
+
});
|
|
73
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({
|
|
74
|
+
forward_url: 'https://other.example.com/fwd',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
it('preserves empty-string sentinel (explicit clear)', async () => {
|
|
78
|
+
await webhook.updateEndpoint(API_KEY, ORG_ID, ENDPOINT_ID, { forward_url: '' });
|
|
79
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ forward_url: '' });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('webhook.EXPOSES — current contract', () => {
|
|
83
|
+
it('includes PATCH for endpoints (2026-05-15)', () => {
|
|
84
|
+
expect(webhook.EXPOSES).toContain('PATCH /webhook/orgs/{org_id}/endpoints/{endpoint_id}');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -59,6 +59,8 @@ Resolution at register time (highest wins): `--registrant-json` → per-field fl
|
|
|
59
59
|
| `myapi domain records create <domain> --type T --name n --content c` | Create a record (priority required for MX) |
|
|
60
60
|
| `myapi domain records update <domain> <id> [--content c] [...]` | Update a record (type cannot change) |
|
|
61
61
|
| `myapi domain records delete <domain> <id> --yes` | Delete a record |
|
|
62
|
+
| `myapi domain email-setup <domain> [--subdomain <label>]` | Opt in to MyAPI-managed email on a subdomain (default: `mail.<domain>`). Apex is never touched |
|
|
63
|
+
| `myapi domain retry-provisioning <domain>` | Re-run provisioning when status=infra_error and error_detail.retryable=true |
|
|
62
64
|
<!-- generated:end -->
|
|
63
65
|
|
|
64
66
|
## Examples
|
|
@@ -91,6 +93,14 @@ myapi domain records list example.com --type TXT
|
|
|
91
93
|
myapi domain records delete example.com <bad-id> --yes
|
|
92
94
|
myapi domain records create example.com --type TXT --name @ \
|
|
93
95
|
--content 'v=spf1 include:_spf.google.com ~all'
|
|
96
|
+
|
|
97
|
+
# Opt in to MyAPI email on a subdomain (apex Google Workspace stays untouched)
|
|
98
|
+
myapi domain email-setup example.com # → mail.example.com
|
|
99
|
+
myapi domain email-setup example.com --subdomain=notifications
|
|
100
|
+
|
|
101
|
+
# Recover from infra_error
|
|
102
|
+
myapi domain status example.com # CLI prints the retry hint
|
|
103
|
+
myapi domain retry-provisioning example.com
|
|
94
104
|
```
|
|
95
105
|
|
|
96
106
|
Security levels: `essentially_off` · `low` · `medium` · `high` · `under_attack`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.5",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"lint:changelog": "node ../../scripts/lint-changelog.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@myapihq/sdk": "^1.2.
|
|
30
|
+
"@myapihq/sdk": "^1.2.5",
|
|
31
31
|
"omelette": "^0.4.17"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|