@serve.zone/dcrouter 17.0.0 → 17.1.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/.smartconfig.json +5 -2
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +110 -49
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -2
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +0 -34
- package/dist_ts/plugins.js +1 -62
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +6 -0
- package/dist_ts_migrations/index.js +204 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +22 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +121 -54
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -1
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
- package/ts/email/classes.workapp-mail-manager.ts +34 -1
- package/ts/email/index.ts +2 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +0 -68
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- package/ts_web/elements/overview/ops-view-config.ts +4 -4
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -648,7 +648,12 @@ export class DnsManager {
|
|
|
648
648
|
* Force-resync a provider-managed domain: re-pull all records from the
|
|
649
649
|
* provider API, replacing the cached DnsRecordDocs.
|
|
650
650
|
*/
|
|
651
|
-
public async syncDomain(id: string): Promise<{
|
|
651
|
+
public async syncDomain(id: string): Promise<{
|
|
652
|
+
success: boolean;
|
|
653
|
+
recordCount?: number;
|
|
654
|
+
listedProviderRecordIds?: string[];
|
|
655
|
+
message?: string;
|
|
656
|
+
}> {
|
|
652
657
|
const doc = await DomainDoc.findById(id);
|
|
653
658
|
if (!doc) return { success: false, message: 'Domain not found' };
|
|
654
659
|
if (doc.source !== 'provider' || !doc.providerId) {
|
|
@@ -658,21 +663,39 @@ export class DnsManager {
|
|
|
658
663
|
if (!client) {
|
|
659
664
|
return { success: false, message: 'Provider client unavailable' };
|
|
660
665
|
}
|
|
661
|
-
|
|
666
|
+
let providerRecords: IProviderRecord[];
|
|
667
|
+
try {
|
|
668
|
+
providerRecords = await client.listRecords(doc.name);
|
|
669
|
+
} catch (error: unknown) {
|
|
670
|
+
return { success: false, message: `Provider list failed: ${(error as Error).message}` };
|
|
671
|
+
}
|
|
672
|
+
const providerIds = providerRecords.map((record) => record.providerRecordId.trim());
|
|
673
|
+
if (providerIds.some((providerId) => !providerId)) {
|
|
674
|
+
return { success: false, message: 'Provider returned a record without a non-empty provider id' };
|
|
675
|
+
}
|
|
676
|
+
if (new Set(providerIds).size !== providerIds.length) {
|
|
677
|
+
return { success: false, message: 'Provider returned duplicate provider record ids' };
|
|
678
|
+
}
|
|
662
679
|
|
|
663
|
-
//
|
|
664
|
-
//
|
|
680
|
+
// Provider ids are the only identity proof. Tuple matching could adopt an
|
|
681
|
+
// operator replacement and transfer reconciler ownership to the wrong row.
|
|
665
682
|
const existing = await DnsRecordDoc.findByDomainId(id);
|
|
683
|
+
const localByProviderId = new Map<string, DnsRecordDoc>();
|
|
684
|
+
for (const record of existing) {
|
|
685
|
+
const providerId = record.providerRecordId?.trim();
|
|
686
|
+
if (!providerId) continue;
|
|
687
|
+
if (localByProviderId.has(providerId)) {
|
|
688
|
+
return { success: false, message: `Local mirror has duplicate provider record id ${providerId}` };
|
|
689
|
+
}
|
|
690
|
+
localByProviderId.set(providerId, record);
|
|
691
|
+
}
|
|
666
692
|
const remaining = new Set(existing);
|
|
667
|
-
for (
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
&& record.type === pr.type
|
|
672
|
-
&& record.value.trim() === pr.value.trim()
|
|
673
|
-
));
|
|
693
|
+
for (let index = 0; index < providerRecords.length; index++) {
|
|
694
|
+
const pr = providerRecords[index];
|
|
695
|
+
const providerId = providerIds[index];
|
|
696
|
+
const match = localByProviderId.get(providerId);
|
|
674
697
|
if (!match) {
|
|
675
|
-
await this.createSyncedRecord(id, pr, doc.createdBy);
|
|
698
|
+
await this.createSyncedRecord(id, { ...pr, providerRecordId: providerId }, doc.createdBy);
|
|
676
699
|
continue;
|
|
677
700
|
}
|
|
678
701
|
remaining.delete(match);
|
|
@@ -682,17 +705,22 @@ export class DnsManager {
|
|
|
682
705
|
match.ttl = pr.ttl;
|
|
683
706
|
match.proxied = pr.proxied;
|
|
684
707
|
match.source = 'synced';
|
|
685
|
-
match.providerRecordId =
|
|
708
|
+
match.providerRecordId = providerId;
|
|
686
709
|
match.updatedAt = Date.now();
|
|
687
710
|
await match.save();
|
|
688
711
|
}
|
|
689
712
|
for (const staleMirror of remaining) {
|
|
713
|
+
if (staleMirror.source !== 'synced' || !staleMirror.providerRecordId?.trim()) continue;
|
|
690
714
|
await staleMirror.delete();
|
|
691
715
|
}
|
|
692
716
|
doc.lastSyncedAt = Date.now();
|
|
693
717
|
doc.updatedAt = doc.lastSyncedAt;
|
|
694
718
|
await doc.save();
|
|
695
|
-
return {
|
|
719
|
+
return {
|
|
720
|
+
success: true,
|
|
721
|
+
recordCount: providerRecords.length,
|
|
722
|
+
listedProviderRecordIds: providerIds,
|
|
723
|
+
};
|
|
696
724
|
}
|
|
697
725
|
|
|
698
726
|
// ==========================================================================
|
|
@@ -900,7 +928,9 @@ export class DnsManager {
|
|
|
900
928
|
ttl: doc.ttl,
|
|
901
929
|
proxied: doc.proxied,
|
|
902
930
|
});
|
|
903
|
-
|
|
931
|
+
const providerRecordId = created.providerRecordId.trim();
|
|
932
|
+
if (!providerRecordId) throw new Error('Provider returned an empty record id');
|
|
933
|
+
doc.providerRecordId = providerRecordId;
|
|
904
934
|
doc.source = 'synced';
|
|
905
935
|
} catch (err: unknown) {
|
|
906
936
|
return { success: false, message: `Provider rejected record: ${(err as Error).message}` };
|
|
@@ -1007,6 +1037,10 @@ export class DnsManager {
|
|
|
1007
1037
|
pr: IProviderRecord,
|
|
1008
1038
|
createdBy: string,
|
|
1009
1039
|
): Promise<void> {
|
|
1040
|
+
const providerRecordId = pr.providerRecordId.trim();
|
|
1041
|
+
if (!providerRecordId) {
|
|
1042
|
+
throw new Error('Cannot mirror a provider record without a non-empty provider id');
|
|
1043
|
+
}
|
|
1010
1044
|
const now = Date.now();
|
|
1011
1045
|
const doc = new DnsRecordDoc();
|
|
1012
1046
|
doc.id = plugins.uuid.v4();
|
|
@@ -1017,7 +1051,7 @@ export class DnsManager {
|
|
|
1017
1051
|
doc.ttl = pr.ttl;
|
|
1018
1052
|
if (pr.proxied !== undefined) doc.proxied = pr.proxied;
|
|
1019
1053
|
doc.source = 'synced';
|
|
1020
|
-
doc.providerRecordId =
|
|
1054
|
+
doc.providerRecordId = providerRecordId;
|
|
1021
1055
|
doc.createdAt = now;
|
|
1022
1056
|
doc.updatedAt = now;
|
|
1023
1057
|
doc.createdBy = createdBy;
|
|
@@ -8,6 +8,10 @@ import type {
|
|
|
8
8
|
} from './interfaces.js';
|
|
9
9
|
import type { IProviderDomainListing } from '../../../ts_interfaces/data/dns-provider.js';
|
|
10
10
|
import type { TDnsRecordType } from '../../../ts_interfaces/data/dns-record.js';
|
|
11
|
+
import {
|
|
12
|
+
decodeTxtRecordContent,
|
|
13
|
+
encodeCloudflareTxtContent,
|
|
14
|
+
} from '../txt-record-presentation.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Cloudflare implementation of IDnsProviderClient.
|
|
@@ -120,6 +124,9 @@ export class CloudflareDnsProvider implements IDnsProviderClient {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
private toApiRecordValue(record: IProviderRecordInput): { content: string; priority?: number } {
|
|
127
|
+
if (record.type === 'TXT') {
|
|
128
|
+
return { content: encodeCloudflareTxtContent(record.value) };
|
|
129
|
+
}
|
|
123
130
|
if (record.type !== 'MX') {
|
|
124
131
|
return { content: record.value };
|
|
125
132
|
}
|
|
@@ -135,9 +142,20 @@ export class CloudflareDnsProvider implements IDnsProviderClient {
|
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
private fromApiRecord(record: any): IProviderRecord {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
let value: string;
|
|
146
|
+
if (record.type === 'MX') {
|
|
147
|
+
value = `${Number(record.priority)} ${record.content}`;
|
|
148
|
+
} else if (record.type === 'TXT') {
|
|
149
|
+
const decoded = decodeTxtRecordContent(String(record.content));
|
|
150
|
+
if (!decoded.ok) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`CloudflareDnsProvider: malformed TXT content for ${String(record.name)}: ${decoded.error}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
value = decoded.value;
|
|
156
|
+
} else {
|
|
157
|
+
value = String(record.content);
|
|
158
|
+
}
|
|
141
159
|
return {
|
|
142
160
|
providerRecordId: String(record.id),
|
|
143
161
|
name: String(record.name),
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type TTxtContentDecodeResult =
|
|
2
|
+
| { ok: true; value: string; wasPresentation: boolean }
|
|
3
|
+
| { ok: false; error: string };
|
|
4
|
+
|
|
5
|
+
const textEncoder = new TextEncoder();
|
|
6
|
+
const textDecoder = new TextDecoder('utf-8', { fatal: true });
|
|
7
|
+
|
|
8
|
+
function appendCharacterBytes(target: number[], character: string): void {
|
|
9
|
+
target.push(...textEncoder.encode(character));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decode the representation returned by DNS provider APIs for TXT records.
|
|
14
|
+
*
|
|
15
|
+
* Cloudflare uses RFC 1035 presentation syntax (one or more adjacent quoted
|
|
16
|
+
* character strings). Some older API responses and dcrouter mirrors contain
|
|
17
|
+
* the already-decoded payload, so an unquoted input is accepted as canonical.
|
|
18
|
+
* A value that starts as presentation syntax must parse completely; malformed
|
|
19
|
+
* quoting or escaping is never repaired by stripping quote characters.
|
|
20
|
+
*/
|
|
21
|
+
export function decodeTxtRecordContent(input: string): TTxtContentDecodeResult {
|
|
22
|
+
const source = input.trim();
|
|
23
|
+
if (!source.startsWith('"')) {
|
|
24
|
+
return { ok: true, value: source, wasPresentation: false };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bytes: number[] = [];
|
|
28
|
+
let index = 0;
|
|
29
|
+
let chunkCount = 0;
|
|
30
|
+
while (index < source.length) {
|
|
31
|
+
while (/\s/.test(source[index] || '')) index++;
|
|
32
|
+
if (index >= source.length) break;
|
|
33
|
+
if (source[index] !== '"') {
|
|
34
|
+
return { ok: false, error: `Unexpected TXT presentation content at offset ${index}` };
|
|
35
|
+
}
|
|
36
|
+
chunkCount++;
|
|
37
|
+
index++;
|
|
38
|
+
let closed = false;
|
|
39
|
+
while (index < source.length) {
|
|
40
|
+
const character = source[index];
|
|
41
|
+
if (character === '"') {
|
|
42
|
+
index++;
|
|
43
|
+
closed = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (character === '\\') {
|
|
47
|
+
index++;
|
|
48
|
+
if (index >= source.length) {
|
|
49
|
+
return { ok: false, error: 'TXT presentation ends with an incomplete escape' };
|
|
50
|
+
}
|
|
51
|
+
if (/\d/.test(source[index])) {
|
|
52
|
+
const decimalEscape = source.slice(index, index + 3);
|
|
53
|
+
if (!/^\d{3}$/.test(decimalEscape)) {
|
|
54
|
+
return { ok: false, error: `Invalid TXT decimal escape at offset ${index - 1}` };
|
|
55
|
+
}
|
|
56
|
+
const byte = Number.parseInt(decimalEscape, 10);
|
|
57
|
+
if (byte > 255) {
|
|
58
|
+
return { ok: false, error: `TXT decimal escape exceeds 255 at offset ${index - 1}` };
|
|
59
|
+
}
|
|
60
|
+
bytes.push(byte);
|
|
61
|
+
index += 3;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const escapedCodePoint = source.codePointAt(index);
|
|
65
|
+
if (escapedCodePoint === undefined) {
|
|
66
|
+
return { ok: false, error: `Invalid TXT escape at offset ${index - 1}` };
|
|
67
|
+
}
|
|
68
|
+
const escapedCharacter = String.fromCodePoint(escapedCodePoint);
|
|
69
|
+
appendCharacterBytes(bytes, escapedCharacter);
|
|
70
|
+
index += escapedCharacter.length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const codePoint = source.codePointAt(index);
|
|
74
|
+
if (codePoint === undefined) {
|
|
75
|
+
return { ok: false, error: `Invalid TXT character at offset ${index}` };
|
|
76
|
+
}
|
|
77
|
+
const nextCharacter = String.fromCodePoint(codePoint);
|
|
78
|
+
appendCharacterBytes(bytes, nextCharacter);
|
|
79
|
+
index += nextCharacter.length;
|
|
80
|
+
}
|
|
81
|
+
if (!closed) {
|
|
82
|
+
return { ok: false, error: 'TXT presentation contains an unterminated character string' };
|
|
83
|
+
}
|
|
84
|
+
while (/\s/.test(source[index] || '')) index++;
|
|
85
|
+
if (index < source.length && source[index] !== '"') {
|
|
86
|
+
return { ok: false, error: `Unexpected TXT presentation content at offset ${index}` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (chunkCount === 0) {
|
|
91
|
+
return { ok: false, error: 'TXT presentation contains no character strings' };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return { ok: true, value: textDecoder.decode(Uint8Array.from(bytes)), wasPresentation: true };
|
|
95
|
+
} catch {
|
|
96
|
+
return { ok: false, error: 'TXT presentation is not valid UTF-8' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function escapeTxtChunk(chunk: string): string {
|
|
101
|
+
let result = '';
|
|
102
|
+
for (const character of chunk) {
|
|
103
|
+
const codePoint = character.codePointAt(0)!;
|
|
104
|
+
if (character === '"' || character === '\\') {
|
|
105
|
+
result += `\\${character}`;
|
|
106
|
+
} else if (codePoint < 32 || codePoint === 127) {
|
|
107
|
+
for (const byte of textEncoder.encode(character)) {
|
|
108
|
+
result += `\\${byte.toString(10).padStart(3, '0')}`;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
result += character;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Encode a canonical TXT payload as Cloudflare/RFC 1035 presentation text. */
|
|
118
|
+
export function encodeCloudflareTxtContent(value: string): string {
|
|
119
|
+
const chunks: string[] = [];
|
|
120
|
+
let current = '';
|
|
121
|
+
let currentBytes = 0;
|
|
122
|
+
for (const character of value) {
|
|
123
|
+
const characterBytes = textEncoder.encode(character).length;
|
|
124
|
+
if (currentBytes > 0 && currentBytes + characterBytes > 255) {
|
|
125
|
+
chunks.push(current);
|
|
126
|
+
current = '';
|
|
127
|
+
currentBytes = 0;
|
|
128
|
+
}
|
|
129
|
+
current += character;
|
|
130
|
+
currentBytes += characterBytes;
|
|
131
|
+
}
|
|
132
|
+
chunks.push(current);
|
|
133
|
+
return chunks.map((chunk) => `"${escapeTxtChunk(chunk)}"`).join(' ');
|
|
134
|
+
}
|