supersendtx 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +116 -2
- package/dist/index.js +388 -3
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -53,13 +53,96 @@ type SuperSendTXOptions = {
|
|
|
53
53
|
baseUrl?: string;
|
|
54
54
|
fetch?: typeof fetch;
|
|
55
55
|
};
|
|
56
|
+
type DomainStatus = 'pending' | 'verified' | 'failed';
|
|
57
|
+
type Domain = {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
status: DomainStatus | string;
|
|
61
|
+
created_at: string;
|
|
62
|
+
verified_at: string | null;
|
|
63
|
+
};
|
|
64
|
+
type DnsRecord = {
|
|
65
|
+
type: string;
|
|
66
|
+
host: string;
|
|
67
|
+
value: string;
|
|
68
|
+
purpose: string;
|
|
69
|
+
};
|
|
70
|
+
type CreateDomainParams = {
|
|
71
|
+
name: string;
|
|
72
|
+
};
|
|
73
|
+
type CreateDomainResult = {
|
|
74
|
+
domain: Domain;
|
|
75
|
+
records: DnsRecord[];
|
|
76
|
+
};
|
|
77
|
+
type VerifyDomainResult = {
|
|
78
|
+
domain: Domain;
|
|
79
|
+
records: DnsRecord[];
|
|
80
|
+
verified: boolean;
|
|
81
|
+
checks?: {
|
|
82
|
+
ownership: boolean;
|
|
83
|
+
spf: boolean;
|
|
84
|
+
dkim: boolean;
|
|
85
|
+
dmarc: boolean;
|
|
86
|
+
};
|
|
87
|
+
auto?: boolean;
|
|
88
|
+
};
|
|
89
|
+
type DnsProviderName$1 = 'cloudflare' | 'godaddy';
|
|
90
|
+
type ApplyDnsParams = {
|
|
91
|
+
provider: DnsProviderName$1;
|
|
92
|
+
/** Domain id or name (fetched from SuperSend TX for DNS records). */
|
|
93
|
+
idOrName: string;
|
|
94
|
+
cloudflareApiToken?: string;
|
|
95
|
+
cloudflareZoneId?: string;
|
|
96
|
+
godaddyApiKey?: string;
|
|
97
|
+
godaddyApiSecret?: string;
|
|
98
|
+
};
|
|
99
|
+
type ApplyDnsRecordResult$1 = {
|
|
100
|
+
purpose: string;
|
|
101
|
+
host: string;
|
|
102
|
+
action: 'created' | 'updated' | 'unchanged' | 'merged';
|
|
103
|
+
detail?: string;
|
|
104
|
+
};
|
|
105
|
+
type ApplyDnsResult$1 = {
|
|
106
|
+
provider: DnsProviderName$1;
|
|
107
|
+
domain: string;
|
|
108
|
+
results: ApplyDnsRecordResult$1[];
|
|
109
|
+
};
|
|
56
110
|
type ApiErrorBody = {
|
|
57
|
-
error?: {
|
|
111
|
+
error?: string | {
|
|
58
112
|
message?: string;
|
|
59
113
|
details?: unknown;
|
|
60
114
|
};
|
|
61
115
|
};
|
|
62
116
|
|
|
117
|
+
declare class DomainsResource {
|
|
118
|
+
private readonly client;
|
|
119
|
+
constructor(client: SuperSendTX);
|
|
120
|
+
list(): Promise<{
|
|
121
|
+
domains: Domain[];
|
|
122
|
+
}>;
|
|
123
|
+
get(idOrName: string): Promise<{
|
|
124
|
+
domain: Domain;
|
|
125
|
+
records: DnsRecord[];
|
|
126
|
+
}>;
|
|
127
|
+
create(params: CreateDomainParams): Promise<CreateDomainResult>;
|
|
128
|
+
verify(idOrName: string): Promise<VerifyDomainResult>;
|
|
129
|
+
delete(idOrName: string): Promise<{
|
|
130
|
+
ok: boolean;
|
|
131
|
+
}>;
|
|
132
|
+
/**
|
|
133
|
+
* Apply DNS using a Cloudflare token stored in the SuperSend TX dashboard
|
|
134
|
+
* (Settings → Integrations).
|
|
135
|
+
*/
|
|
136
|
+
apply(idOrName: string, options?: {
|
|
137
|
+
provider?: 'cloudflare';
|
|
138
|
+
}): Promise<ApplyDnsResult$1>;
|
|
139
|
+
/**
|
|
140
|
+
* Fetch SuperSend TX DNS records, then write them at Cloudflare or GoDaddy
|
|
141
|
+
* using the caller's provider credentials (never sent to SuperSend TX).
|
|
142
|
+
*/
|
|
143
|
+
applyDns(params: ApplyDnsParams): Promise<ApplyDnsResult$1>;
|
|
144
|
+
}
|
|
145
|
+
|
|
63
146
|
declare class EmailsResource {
|
|
64
147
|
private readonly client;
|
|
65
148
|
constructor(client: SuperSendTX);
|
|
@@ -85,6 +168,7 @@ declare const DEFAULT_API_BASE_URL = "https://api.supersendtx.com";
|
|
|
85
168
|
declare class SuperSendTX {
|
|
86
169
|
private readonly apiKey;
|
|
87
170
|
private readonly options;
|
|
171
|
+
readonly domains: DomainsResource;
|
|
88
172
|
readonly emails: EmailsResource;
|
|
89
173
|
readonly webhooks: WebhooksResource;
|
|
90
174
|
constructor(apiKey: string, options?: SuperSendTXOptions);
|
|
@@ -100,4 +184,34 @@ declare class SuperSendTXError extends Error {
|
|
|
100
184
|
static fromResponse(status: number, body: ApiErrorBody): SuperSendTXError;
|
|
101
185
|
}
|
|
102
186
|
|
|
103
|
-
|
|
187
|
+
/** Merge `include:host` into an existing SPF record (or create a minimal one). */
|
|
188
|
+
declare function mergeSpfInclude(existing: string | null | undefined, includeHost: string): string;
|
|
189
|
+
|
|
190
|
+
type DnsProviderName = 'cloudflare' | 'godaddy';
|
|
191
|
+
type ApplyDnsRecordResult = {
|
|
192
|
+
purpose: string;
|
|
193
|
+
host: string;
|
|
194
|
+
action: 'created' | 'updated' | 'unchanged' | 'merged';
|
|
195
|
+
detail?: string;
|
|
196
|
+
};
|
|
197
|
+
type ApplyDnsResult = {
|
|
198
|
+
provider: DnsProviderName;
|
|
199
|
+
domain: string;
|
|
200
|
+
results: ApplyDnsRecordResult[];
|
|
201
|
+
};
|
|
202
|
+
type ApplyDnsCredentials = {
|
|
203
|
+
cloudflareApiToken?: string;
|
|
204
|
+
cloudflareZoneId?: string;
|
|
205
|
+
godaddyApiKey?: string;
|
|
206
|
+
godaddyApiSecret?: string;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
declare function applyDnsRecords(input: {
|
|
210
|
+
provider: DnsProviderName;
|
|
211
|
+
domain: string;
|
|
212
|
+
records: DnsRecord[];
|
|
213
|
+
credentials: ApplyDnsCredentials;
|
|
214
|
+
fetchImpl?: typeof fetch;
|
|
215
|
+
}): Promise<ApplyDnsResult>;
|
|
216
|
+
|
|
217
|
+
export { type ApplyDnsParams, type ApplyDnsRecordResult$1 as ApplyDnsRecordResult, type ApplyDnsResult$1 as ApplyDnsResult, type CreateDomainParams, type CreateDomainResult, type CreateWebhookParams, DEFAULT_API_BASE_URL, type DnsProviderName$1 as DnsProviderName, type DnsRecord, type Domain, type DomainStatus, type EmailListItem, type ListEmailsParams, type SendEmailParams, type SendEmailResult, SuperSendTX, SuperSendTXError, type SuperSendTXOptions, type VerifyDomainResult, type WebhookEndpoint, type WebhookEndpointWithSecret, type WebhookEventType, applyDnsRecords, mergeSpfInclude };
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,390 @@ var SuperSendTXError = class _SuperSendTXError extends Error {
|
|
|
7
7
|
this.details = details;
|
|
8
8
|
}
|
|
9
9
|
static fromResponse(status, body) {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const err = body.error;
|
|
11
|
+
const message = typeof err === "string" ? err : err?.message || `Request failed with status ${status}`;
|
|
12
|
+
const details = typeof err === "object" && err ? err.details : void 0;
|
|
13
|
+
return new _SuperSendTXError(message, status, details);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/dns/spf.ts
|
|
18
|
+
function mergeSpfInclude(existing, includeHost) {
|
|
19
|
+
const include = `include:${includeHost.replace(/^include:/i, "")}`;
|
|
20
|
+
const trimmed = existing?.trim();
|
|
21
|
+
if (!trimmed) {
|
|
22
|
+
return `v=spf1 ${include} ~all`;
|
|
23
|
+
}
|
|
24
|
+
if (trimmed.toLowerCase().includes(include.toLowerCase())) {
|
|
25
|
+
return trimmed;
|
|
26
|
+
}
|
|
27
|
+
if (!/^v=spf1\b/i.test(trimmed)) {
|
|
28
|
+
return `v=spf1 ${include} ~all`;
|
|
29
|
+
}
|
|
30
|
+
if (/\s[~?+\-]all\s*$/i.test(trimmed)) {
|
|
31
|
+
return trimmed.replace(/\s([~?+\-]all)\s*$/i, ` ${include} $1`);
|
|
32
|
+
}
|
|
33
|
+
return `${trimmed} ${include} ~all`;
|
|
34
|
+
}
|
|
35
|
+
function isSpfRecord(value) {
|
|
36
|
+
return value.trim().toLowerCase().startsWith("v=spf1");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/dns/cloudflare.ts
|
|
40
|
+
function relativeName(fqdn, zone) {
|
|
41
|
+
const domain = zone.replace(/\.$/, "").toLowerCase();
|
|
42
|
+
const host = fqdn.replace(/\.$/, "").toLowerCase();
|
|
43
|
+
if (host === domain) return "@";
|
|
44
|
+
if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
|
|
45
|
+
return host;
|
|
46
|
+
}
|
|
47
|
+
function createCloudflareProvider(options) {
|
|
48
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
49
|
+
const headers = {
|
|
50
|
+
Authorization: `Bearer ${options.apiToken}`,
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
};
|
|
53
|
+
async function cf(path, init) {
|
|
54
|
+
const response = await fetchImpl(`https://api.cloudflare.com/client/v4${path}`, {
|
|
55
|
+
...init,
|
|
56
|
+
headers: { ...headers, ...init?.headers || {} }
|
|
57
|
+
});
|
|
58
|
+
const body = await response.json().catch(() => ({}));
|
|
59
|
+
if (!response.ok || body.success === false) {
|
|
60
|
+
const message = body.errors?.map((e) => e.message).filter(Boolean).join("; ") || `Cloudflare HTTP ${response.status}`;
|
|
61
|
+
throw new Error(message);
|
|
62
|
+
}
|
|
63
|
+
return body.result;
|
|
64
|
+
}
|
|
65
|
+
async function resolveZoneId(domain) {
|
|
66
|
+
if (options.zoneId) return options.zoneId;
|
|
67
|
+
const zones = await cf(`/zones?name=${encodeURIComponent(domain)}&status=active`);
|
|
68
|
+
const zone = zones?.[0];
|
|
69
|
+
if (!zone) {
|
|
70
|
+
throw new Error(`No active Cloudflare zone found for ${domain}`);
|
|
71
|
+
}
|
|
72
|
+
return zone.id;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: "cloudflare",
|
|
76
|
+
async apply(domain, records) {
|
|
77
|
+
const zoneId = await resolveZoneId(domain);
|
|
78
|
+
const zoneName = domain.toLowerCase();
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const record of records) {
|
|
81
|
+
if (record.type.toUpperCase() !== "TXT") {
|
|
82
|
+
results.push({
|
|
83
|
+
purpose: record.purpose,
|
|
84
|
+
host: record.host,
|
|
85
|
+
action: "unchanged",
|
|
86
|
+
detail: `Skipped unsupported type ${record.type}`
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const name = relativeName(record.host, zoneName);
|
|
91
|
+
const absoluteName = name === "@" ? zoneName : `${name}.${zoneName}`;
|
|
92
|
+
if (record.purpose === "SPF") {
|
|
93
|
+
const allTxt = await cf(
|
|
94
|
+
`/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(zoneName)}&per_page=100`
|
|
95
|
+
);
|
|
96
|
+
const spfRecords = (allTxt || []).filter((r) => isSpfRecord(r.content));
|
|
97
|
+
const existing = spfRecords[0];
|
|
98
|
+
const includeHost = record.value.match(/include:([^\s]+)/i)?.[1] || "spf.supersendtx.com";
|
|
99
|
+
const merged = mergeSpfInclude(existing?.content, includeHost);
|
|
100
|
+
if (existing) {
|
|
101
|
+
if (existing.content.trim() === merged.trim()) {
|
|
102
|
+
results.push({
|
|
103
|
+
purpose: record.purpose,
|
|
104
|
+
host: record.host,
|
|
105
|
+
action: "unchanged",
|
|
106
|
+
detail: "SPF already includes SuperSend TX"
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
await cf(`/zones/${zoneId}/dns_records/${existing.id}`, {
|
|
110
|
+
method: "PUT",
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
type: "TXT",
|
|
113
|
+
name: zoneName,
|
|
114
|
+
content: merged,
|
|
115
|
+
ttl: 1
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
results.push({
|
|
119
|
+
purpose: record.purpose,
|
|
120
|
+
host: record.host,
|
|
121
|
+
action: "merged",
|
|
122
|
+
detail: merged
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
type: "TXT",
|
|
130
|
+
name: zoneName,
|
|
131
|
+
content: merged,
|
|
132
|
+
ttl: 1
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
results.push({
|
|
136
|
+
purpose: record.purpose,
|
|
137
|
+
host: record.host,
|
|
138
|
+
action: "created",
|
|
139
|
+
detail: merged
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const existingList = await cf(
|
|
145
|
+
`/zones/${zoneId}/dns_records?type=TXT&name=${encodeURIComponent(absoluteName)}&per_page=100`
|
|
146
|
+
);
|
|
147
|
+
const match = (existingList || []).find((r) => r.content === record.value);
|
|
148
|
+
if (match) {
|
|
149
|
+
results.push({
|
|
150
|
+
purpose: record.purpose,
|
|
151
|
+
host: record.host,
|
|
152
|
+
action: "unchanged"
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const replaceable = (existingList || []).find((r) => {
|
|
157
|
+
if (record.purpose === "Domain ownership") return r.content.includes("supersendtx-verify=");
|
|
158
|
+
if (record.purpose === "DKIM") return r.content.toLowerCase().includes("v=dkim1");
|
|
159
|
+
if (record.purpose === "DMARC") return r.content.toLowerCase().includes("v=dmarc1");
|
|
160
|
+
return false;
|
|
161
|
+
});
|
|
162
|
+
if (replaceable) {
|
|
163
|
+
await cf(`/zones/${zoneId}/dns_records/${replaceable.id}`, {
|
|
164
|
+
method: "PUT",
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
type: "TXT",
|
|
167
|
+
name: absoluteName,
|
|
168
|
+
content: record.value,
|
|
169
|
+
ttl: 1
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
results.push({ purpose: record.purpose, host: record.host, action: "updated" });
|
|
173
|
+
} else {
|
|
174
|
+
await cf(`/zones/${zoneId}/dns_records`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
type: "TXT",
|
|
178
|
+
name: absoluteName,
|
|
179
|
+
content: record.value,
|
|
180
|
+
ttl: 1
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
results.push({ purpose: record.purpose, host: record.host, action: "created" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/dns/godaddy.ts
|
|
192
|
+
function relativeName2(fqdn, zone) {
|
|
193
|
+
const domain = zone.replace(/\.$/, "").toLowerCase();
|
|
194
|
+
const host = fqdn.replace(/\.$/, "").toLowerCase();
|
|
195
|
+
if (host === domain) return "@";
|
|
196
|
+
if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
|
|
197
|
+
return host;
|
|
198
|
+
}
|
|
199
|
+
function createGoDaddyProvider(options) {
|
|
200
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
201
|
+
const headers = {
|
|
202
|
+
Authorization: `sso-key ${options.apiKey}:${options.apiSecret}`,
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
Accept: "application/json"
|
|
205
|
+
};
|
|
206
|
+
async function gd(path, init) {
|
|
207
|
+
const response = await fetchImpl(`https://api.godaddy.com${path}`, {
|
|
208
|
+
...init,
|
|
209
|
+
headers: { ...headers, ...init?.headers || {} }
|
|
210
|
+
});
|
|
211
|
+
if (response.status === 204) return void 0;
|
|
212
|
+
const body = await response.json().catch(() => ({}));
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
const message = body.message || (Array.isArray(body.errors) ? body.errors.map((e) => e.message).join("; ") : null) || `GoDaddy HTTP ${response.status}`;
|
|
215
|
+
throw new Error(message);
|
|
216
|
+
}
|
|
217
|
+
return body;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
name: "godaddy",
|
|
221
|
+
async apply(domain, records) {
|
|
222
|
+
const zone = domain.toLowerCase();
|
|
223
|
+
const results = [];
|
|
224
|
+
for (const record of records) {
|
|
225
|
+
if (record.type.toUpperCase() !== "TXT") {
|
|
226
|
+
results.push({
|
|
227
|
+
purpose: record.purpose,
|
|
228
|
+
host: record.host,
|
|
229
|
+
action: "unchanged",
|
|
230
|
+
detail: `Skipped unsupported type ${record.type}`
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const name = relativeName2(record.host, zone);
|
|
235
|
+
if (record.purpose === "SPF") {
|
|
236
|
+
const existing2 = await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`);
|
|
237
|
+
const list2 = existing2 || [];
|
|
238
|
+
const spfIndex = list2.findIndex((r) => isSpfRecord(r.data));
|
|
239
|
+
const includeHost = record.value.match(/include:([^\s]+)/i)?.[1] || "spf.supersendtx.com";
|
|
240
|
+
const currentSpf = spfIndex >= 0 ? list2[spfIndex].data : null;
|
|
241
|
+
const merged = mergeSpfInclude(currentSpf, includeHost);
|
|
242
|
+
if (spfIndex >= 0 && list2[spfIndex].data.trim() === merged.trim()) {
|
|
243
|
+
results.push({
|
|
244
|
+
purpose: record.purpose,
|
|
245
|
+
host: record.host,
|
|
246
|
+
action: "unchanged",
|
|
247
|
+
detail: "SPF already includes SuperSend TX"
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const next2 = list2.map(
|
|
252
|
+
(r, i) => i === spfIndex ? { type: "TXT", name, data: merged, ttl: Math.max(r.ttl || 600, 600) } : r
|
|
253
|
+
);
|
|
254
|
+
if (spfIndex < 0) {
|
|
255
|
+
next2.push({ type: "TXT", name, data: merged, ttl: 600 });
|
|
256
|
+
}
|
|
257
|
+
await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`, {
|
|
258
|
+
method: "PUT",
|
|
259
|
+
body: JSON.stringify(next2.map(({ type, name: n, data, ttl }) => ({ type, name: n, data, ttl })))
|
|
260
|
+
});
|
|
261
|
+
results.push({
|
|
262
|
+
purpose: record.purpose,
|
|
263
|
+
host: record.host,
|
|
264
|
+
action: spfIndex >= 0 ? "merged" : "created",
|
|
265
|
+
detail: merged
|
|
266
|
+
});
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const existing = await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`);
|
|
270
|
+
const list = existing || [];
|
|
271
|
+
const exact = list.find((r) => r.data === record.value);
|
|
272
|
+
if (exact) {
|
|
273
|
+
results.push({ purpose: record.purpose, host: record.host, action: "unchanged" });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const replaceableIndex = list.findIndex((r) => {
|
|
277
|
+
if (record.purpose === "Domain ownership") return r.data.includes("supersendtx-verify=");
|
|
278
|
+
if (record.purpose === "DKIM") return r.data.toLowerCase().includes("v=dkim1");
|
|
279
|
+
if (record.purpose === "DMARC") return r.data.toLowerCase().includes("v=dmarc1");
|
|
280
|
+
return false;
|
|
281
|
+
});
|
|
282
|
+
const next = replaceableIndex >= 0 ? list.map(
|
|
283
|
+
(r, i) => i === replaceableIndex ? { type: "TXT", name, data: record.value, ttl: Math.max(r.ttl || 600, 600) } : r
|
|
284
|
+
) : [...list, { type: "TXT", name, data: record.value, ttl: 600 }];
|
|
285
|
+
await gd(`/v1/domains/${zone}/records/TXT/${encodeURIComponent(name)}`, {
|
|
286
|
+
method: "PUT",
|
|
287
|
+
body: JSON.stringify(next.map(({ type, name: n, data, ttl }) => ({ type, name: n, data, ttl })))
|
|
288
|
+
});
|
|
289
|
+
results.push({
|
|
290
|
+
purpose: record.purpose,
|
|
291
|
+
host: record.host,
|
|
292
|
+
action: replaceableIndex >= 0 ? "updated" : "created"
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/dns/apply.ts
|
|
301
|
+
function createDnsProvider(provider, credentials, fetchImpl) {
|
|
302
|
+
if (provider === "cloudflare") {
|
|
303
|
+
const token = credentials.cloudflareApiToken;
|
|
304
|
+
if (!token) {
|
|
305
|
+
throw new Error("Missing Cloudflare token. Set CLOUDFLARE_API_TOKEN or pass cloudflareApiToken.");
|
|
306
|
+
}
|
|
307
|
+
return createCloudflareProvider({
|
|
308
|
+
apiToken: token,
|
|
309
|
+
zoneId: credentials.cloudflareZoneId,
|
|
310
|
+
fetchImpl
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (provider === "godaddy") {
|
|
314
|
+
const apiKey = credentials.godaddyApiKey;
|
|
315
|
+
const apiSecret = credentials.godaddyApiSecret;
|
|
316
|
+
if (!apiKey || !apiSecret) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"Missing GoDaddy credentials. Set GODADDY_API_KEY and GODADDY_API_SECRET."
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return createGoDaddyProvider({ apiKey, apiSecret, fetchImpl });
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`Unsupported DNS provider: ${provider}`);
|
|
324
|
+
}
|
|
325
|
+
async function applyDnsRecords(input) {
|
|
326
|
+
const provider = createDnsProvider(input.provider, input.credentials, input.fetchImpl);
|
|
327
|
+
const results = await provider.apply(input.domain, input.records);
|
|
328
|
+
return {
|
|
329
|
+
provider: input.provider,
|
|
330
|
+
domain: input.domain,
|
|
331
|
+
results
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/domains.ts
|
|
336
|
+
var DomainsResource = class {
|
|
337
|
+
constructor(client) {
|
|
338
|
+
this.client = client;
|
|
339
|
+
}
|
|
340
|
+
async list() {
|
|
341
|
+
return this.client.request("/domains", { method: "GET" });
|
|
342
|
+
}
|
|
343
|
+
async get(idOrName) {
|
|
344
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, { method: "GET" });
|
|
345
|
+
}
|
|
346
|
+
async create(params) {
|
|
347
|
+
return this.client.request("/domains", {
|
|
348
|
+
method: "POST",
|
|
349
|
+
body: JSON.stringify(params)
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
async verify(idOrName) {
|
|
353
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
body: JSON.stringify({ action: "verify" })
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async delete(idOrName) {
|
|
359
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
360
|
+
method: "DELETE"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Apply DNS using a Cloudflare token stored in the SuperSend TX dashboard
|
|
365
|
+
* (Settings → Integrations).
|
|
366
|
+
*/
|
|
367
|
+
async apply(idOrName, options = {}) {
|
|
368
|
+
return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
action: "apply",
|
|
372
|
+
provider: options.provider || "cloudflare"
|
|
373
|
+
})
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Fetch SuperSend TX DNS records, then write them at Cloudflare or GoDaddy
|
|
378
|
+
* using the caller's provider credentials (never sent to SuperSend TX).
|
|
379
|
+
*/
|
|
380
|
+
async applyDns(params) {
|
|
381
|
+
const { domain, records } = await this.get(params.idOrName);
|
|
382
|
+
return applyDnsRecords({
|
|
383
|
+
provider: params.provider,
|
|
384
|
+
domain: domain.name,
|
|
385
|
+
records,
|
|
386
|
+
credentials: {
|
|
387
|
+
cloudflareApiToken: params.cloudflareApiToken,
|
|
388
|
+
cloudflareZoneId: params.cloudflareZoneId,
|
|
389
|
+
godaddyApiKey: params.godaddyApiKey,
|
|
390
|
+
godaddyApiSecret: params.godaddyApiSecret
|
|
391
|
+
},
|
|
392
|
+
fetchImpl: this.client.fetchImpl
|
|
393
|
+
});
|
|
12
394
|
}
|
|
13
395
|
};
|
|
14
396
|
|
|
@@ -70,6 +452,7 @@ var SuperSendTX = class {
|
|
|
70
452
|
if (!apiKey?.startsWith("stx_")) {
|
|
71
453
|
throw new Error("SuperSend TX API key must start with stx_");
|
|
72
454
|
}
|
|
455
|
+
this.domains = new DomainsResource(this);
|
|
73
456
|
this.emails = new EmailsResource(this);
|
|
74
457
|
this.webhooks = new WebhooksResource(this);
|
|
75
458
|
}
|
|
@@ -98,5 +481,7 @@ var SuperSendTX = class {
|
|
|
98
481
|
export {
|
|
99
482
|
DEFAULT_API_BASE_URL,
|
|
100
483
|
SuperSendTX,
|
|
101
|
-
SuperSendTXError
|
|
484
|
+
SuperSendTXError,
|
|
485
|
+
applyDnsRecords,
|
|
486
|
+
mergeSpfInclude
|
|
102
487
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supersendtx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "SuperSend TX transactional email API client",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
18
20
|
"url": "git+https://github.com/Super-Send/superTX-app.git",
|