domain0 0.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/README.md +629 -0
- package/dist/contracts.cjs +1773 -0
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.cts +2 -0
- package/dist/contracts.d.mts +2 -0
- package/dist/contracts.mjs +1627 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/error-BB4-lyc4.cjs +48 -0
- package/dist/error-BB4-lyc4.cjs.map +1 -0
- package/dist/error-Cyd6a9ay.mjs +31 -0
- package/dist/error-Cyd6a9ay.mjs.map +1 -0
- package/dist/index-CtQn01os.d.cts +6930 -0
- package/dist/index-CtQn01os.d.mts +6930 -0
- package/dist/index.cjs +246 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +91 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sdk-7CglngBt.d.mts +196 -0
- package/dist/sdk-BHIxsxUT.mjs +3997 -0
- package/dist/sdk-BHIxsxUT.mjs.map +1 -0
- package/dist/sdk-BKtsupNk.d.cts +196 -0
- package/dist/sdk-C0q5dV-6.cjs +4074 -0
- package/dist/sdk-C0q5dV-6.cjs.map +1 -0
- package/dist/server.cjs +103 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +19 -0
- package/dist/server.d.mts +19 -0
- package/dist/server.mjs +102 -0
- package/dist/server.mjs.map +1 -0
- package/dist/ui.cjs +12 -0
- package/dist/ui.d.cts +286 -0
- package/dist/ui.d.mts +286 -0
- package/dist/ui.mjs +2 -0
- package/package.json +112 -0
package/README.md
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
# Domain0 TypeScript SDK
|
|
2
|
+
|
|
3
|
+
This package contains Domain0's type-safe HTTP client,
|
|
4
|
+
Zod contracts, and dependency-free embeddable connection UI.
|
|
5
|
+
Node.js consumers require a currently supported Node.js 22 or newer release;
|
|
6
|
+
browser consumers need standards-compliant `fetch`, Web Crypto, and `<dialog>`.
|
|
7
|
+
|
|
8
|
+
The hosted platform uses two explicit trust boundaries. Keep the long-lived
|
|
9
|
+
API key on the integrating server, then give the browser only short-lived,
|
|
10
|
+
exact-origin access to one connection:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createDomain0PlatformClient } from 'domain0'
|
|
14
|
+
|
|
15
|
+
const platform = createDomain0PlatformClient({
|
|
16
|
+
baseUrl: 'https://domain0.dev/api/domain0/',
|
|
17
|
+
apiKey: process.env.DOMAIN0_API_KEY!,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { connection } = await platform.createConnection({
|
|
21
|
+
intent: {
|
|
22
|
+
domain: 'customer.example',
|
|
23
|
+
records: [{ host: 'app', type: 'CNAME', value: 'edge.example.com' }],
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
const access = await platform.issueConnectionToken(connection.id, {
|
|
27
|
+
origin: 'https://app.example.com',
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use `forwardDomain0ConnectionRequest()` from `domain0/server` in a same-origin
|
|
32
|
+
catch-all route. It forwards only browser-safe connection routes, strips
|
|
33
|
+
cookies and API keys, caps request bodies, and preserves the exact origin.
|
|
34
|
+
The browser constructs `createDomain0Client()` against that local route and
|
|
35
|
+
opens `domain0.connectDomain()` with the returned connection ID.
|
|
36
|
+
|
|
37
|
+
Webhook receivers can import the strict schema-v1 contract separately. The
|
|
38
|
+
verifier authenticates untouched request bytes before parsing them:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { verifyDomain0Webhook } from 'domain0/contracts'
|
|
42
|
+
|
|
43
|
+
const event = await verifyDomain0Webhook({
|
|
44
|
+
body: new Uint8Array(await request.arrayBuffer()),
|
|
45
|
+
headers: request.headers,
|
|
46
|
+
secretKeyring: { 'webhook-v1': decoded32ByteSecret },
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await processOnceInTransaction(event.eventId, () => applyEvent(event))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep the idempotency record and business change in one database transaction.
|
|
53
|
+
Return 2xx when an event ID was already processed.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createDomain0Client } from 'domain0'
|
|
57
|
+
|
|
58
|
+
const client = createDomain0Client({
|
|
59
|
+
baseUrl: 'https://domain0.example.com',
|
|
60
|
+
token: async () => obtainShortLivedConnectionToken(),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const connection = await client.getConnection(connectionId)
|
|
64
|
+
|
|
65
|
+
const providerHealth = await client.getProviderHealth()
|
|
66
|
+
for (const provider of providerHealth.providers) {
|
|
67
|
+
if (!provider.enabled) {
|
|
68
|
+
console.info(provider.providerName, provider.reason)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const currentDns = await client.checkRecords({
|
|
73
|
+
domain: 'example.com',
|
|
74
|
+
records: [{ host: 'www', type: 'CNAME', value: 'edge.example.net', ttl: 300 }],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const support = await client.checkDomain({
|
|
78
|
+
domain: 'example.com',
|
|
79
|
+
records: [{ host: 'www', type: 'CNAME', value: 'edge.example.net' }],
|
|
80
|
+
checkConflicts: true,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await client.authorizeWithCredential(connectionId, {
|
|
84
|
+
providerId: 'cloudflare',
|
|
85
|
+
credentials: {
|
|
86
|
+
kind: 'api_token',
|
|
87
|
+
token: obtainProviderToken(),
|
|
88
|
+
},
|
|
89
|
+
commandId: crypto.randomUUID(),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await client.cancelConnection(connectionId, {
|
|
93
|
+
commandId: crypto.randomUUID(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (connection.connection.state === 'failed_retryable') {
|
|
97
|
+
await client.retryConnection(connectionId, {
|
|
98
|
+
commandId: crypto.randomUUID(),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
An application-authenticated server can provision several exact-domain
|
|
104
|
+
connections with one replay-safe command. A target uses either direct records
|
|
105
|
+
or the conditional record shape—never both:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const created = await applicationClient.createConnectionFlow({
|
|
109
|
+
applicationId: 'application-1',
|
|
110
|
+
tenantId: 'tenant-1',
|
|
111
|
+
commandId: crypto.randomUUID(),
|
|
112
|
+
targets: [
|
|
113
|
+
{
|
|
114
|
+
domain: 'example.com',
|
|
115
|
+
records: [{ host: '@', type: 'A', value: '192.0.2.10', ttl: 300 }],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
domain: 'docs.example.net',
|
|
119
|
+
conditionalRecords: {
|
|
120
|
+
domain: [{ host: '@', type: 'CNAME', value: 'fallback.example.net' }],
|
|
121
|
+
subDomain: [{ host: '@', type: 'CNAME', value: 'edge.example.net' }],
|
|
122
|
+
rootNS: [{ host: '@', type: 'NS', value: 'ns1.example.net' }],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await domain0.connectDomains({
|
|
129
|
+
client: browserConnectionClient,
|
|
130
|
+
flowId: created.flow.id,
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`connectDomains()` reuses the same single-domain controller for one child at a
|
|
135
|
+
time and announces fixed “Domain N of total” progress. Closing does not delete
|
|
136
|
+
server state; reopening the flow ID reloads its durable child references.
|
|
137
|
+
Domain0 derives the registrable-root boundary and resolves `rootNS` only after
|
|
138
|
+
the selected provider's catalogued capability is `supported`. Unverified or
|
|
139
|
+
unsupported providers fall back to `subDomain`, then `domain`; callers cannot
|
|
140
|
+
override either decision.
|
|
141
|
+
|
|
142
|
+
An application-authenticated server can create a time-bounded handoff for an
|
|
143
|
+
existing connection. The URL fragment is not sent in HTTP requests or ordinary
|
|
144
|
+
access logs:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { createSharedFlowUrl, loadSharedFlow } from 'domain0'
|
|
148
|
+
|
|
149
|
+
const shared = await applicationClient.createSharedFlow({
|
|
150
|
+
connectionId,
|
|
151
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
152
|
+
})
|
|
153
|
+
const link = createSharedFlowUrl('https://app.example.com/connect', shared.token)
|
|
154
|
+
|
|
155
|
+
const loaded = await loadSharedFlow({
|
|
156
|
+
baseUrl: 'https://domain0.example.com',
|
|
157
|
+
url: link,
|
|
158
|
+
origin: window.location.origin,
|
|
159
|
+
})
|
|
160
|
+
await loaded.client.getConnection(loaded.connectionId)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`mountDomain0SharedFlow()` combines resolution with the same dependency-free
|
|
164
|
+
connection UI. Shared-flow storage contains only ownership identifiers, a
|
|
165
|
+
SHA-256 token digest, and timestamps—never the bearer token, DNS configuration,
|
|
166
|
+
or provider credentials. Invalid and expired tokens are indistinguishable.
|
|
167
|
+
|
|
168
|
+
Every request is validated before transmission and every successful or error
|
|
169
|
+
response is parsed at runtime. The SDK rejects non-HTTPS base URLs except
|
|
170
|
+
loopback development addresses. Base URLs cannot contain user information,
|
|
171
|
+
query parameters, or fragments, and bearer tokens must match RFC 6750's ASCII
|
|
172
|
+
token grammar before `fetch` is called. Keep application API tokens on the server;
|
|
173
|
+
browsers should receive only short-lived, exact-origin, connection-scoped
|
|
174
|
+
tokens. Credential shapes are a provider-discriminated Zod union, so an
|
|
175
|
+
unsupported provider or credential kind fails before a request is sent.
|
|
176
|
+
`getProviderHealth()` returns every reviewed provider and matches Entri's
|
|
177
|
+
`enabled` compatibility field while adding strict `status`, `reason`,
|
|
178
|
+
`observedAt`, and `refreshAfter` semantics. Enabled means that this Domain0
|
|
179
|
+
deployment exposes a verified non-manual setup path. It is not an upstream
|
|
180
|
+
uptime probe and does not promote an unverified provider.
|
|
181
|
+
`checkRecords()` is an application-token-only, read-only authoritative DNS
|
|
182
|
+
check. It does not create a connection, write provider state, or treat a
|
|
183
|
+
recursive resolver answer as propagation proof; each result reports how many
|
|
184
|
+
authoritative nameservers returned the requested value.
|
|
185
|
+
`checkDomain()` has the same application-token-only boundary. It reports
|
|
186
|
+
`delegated`, `undelegated`, or `unknown` DNS state; provider candidates;
|
|
187
|
+
deployment-backed setup availability; and optional conflict analysis. A
|
|
188
|
+
`partial` analysis means at least one authoritative nameserver could not
|
|
189
|
+
complete every required query, so callers must not treat the result as proof
|
|
190
|
+
that no conflict exists. Domain0 deliberately does not expose `registered` or
|
|
191
|
+
`expired` booleans from DNS evidence because delegation cannot prove either.
|
|
192
|
+
The same union covers API tokens, access-key pairs, username/password,
|
|
193
|
+
account/token, username/token, OVH AK/AS/CK, TransIP PEM keys, temporary AWS
|
|
194
|
+
sessions with explicit expiry, and provider-scoped cPanel credentials.
|
|
195
|
+
Cancellation is an idempotent, typed operation for non-terminal connections;
|
|
196
|
+
it does not reverse DNS records that a provider may already have applied.
|
|
197
|
+
Retry does not blindly repeat a provider mutation. It restores the exact
|
|
198
|
+
pre-failure checkpoint, after which the integrating application or UI starts a
|
|
199
|
+
fresh snapshot or reconciliation attempt explicitly.
|
|
200
|
+
|
|
201
|
+
SPF conflict handling is explicit and typed when preparing a plan. `merge` is
|
|
202
|
+
the safe default and combines unique mechanisms, choosing `~all` when existing
|
|
203
|
+
and requested terminal policies disagree. `replace` uses the requested SPF
|
|
204
|
+
record as-is, marks the change destructive, emits a plan warning, and still
|
|
205
|
+
requires the separate destructive confirmation step:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
await client.preparePlan(connectionId, {
|
|
209
|
+
conflictPolicy: 'preserve',
|
|
210
|
+
spfPolicy: 'replace',
|
|
211
|
+
commandId: crypto.randomUUID(),
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Advanced DMARC options are attached only to a valid `_dmarc` TXT record. They
|
|
216
|
+
are strict application configuration, not arbitrary UI input:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
await applicationClient.createConnection({
|
|
220
|
+
applicationId: 'application-1',
|
|
221
|
+
tenantId: 'tenant-1',
|
|
222
|
+
commandId: crypto.randomUUID(),
|
|
223
|
+
intent: {
|
|
224
|
+
domain: 'mail.example.com',
|
|
225
|
+
records: [{
|
|
226
|
+
host: '_dmarc',
|
|
227
|
+
type: 'TXT',
|
|
228
|
+
value: 'v=DMARC1; p=none',
|
|
229
|
+
ttl: 300,
|
|
230
|
+
advancedDmarcOptions: {
|
|
231
|
+
inheritRootDmarc: true,
|
|
232
|
+
overrideTags: { p: 'quarantine' },
|
|
233
|
+
removeTags: ['pct'],
|
|
234
|
+
addTagsIfNotExist: { aspf: 's' },
|
|
235
|
+
},
|
|
236
|
+
}],
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Planning preserves this requested intent and returns `effectiveIntent` after
|
|
242
|
+
reading existing provider records and, when requested, the registrable root's
|
|
243
|
+
public DMARC TXT record. Guided manual setup resolves the same policy from
|
|
244
|
+
authoritative public DNS. The UI always displays the effective records that
|
|
245
|
+
propagation verification will check. If an existing or inherited DMARC policy
|
|
246
|
+
is invalid or ambiguous, Domain0 uses the supplied valid `value` unchanged
|
|
247
|
+
instead of partially rewriting it.
|
|
248
|
+
|
|
249
|
+
Existing-record checks are a separate strict policy on the requested intent:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
await applicationClient.createConnection({
|
|
253
|
+
applicationId: 'application-1',
|
|
254
|
+
tenantId: 'tenant-1',
|
|
255
|
+
commandId: crypto.randomUUID(),
|
|
256
|
+
intent: {
|
|
257
|
+
domain: 'example.com',
|
|
258
|
+
existingRecordPolicy: {
|
|
259
|
+
validateDmarc: true,
|
|
260
|
+
validateCAA: true,
|
|
261
|
+
},
|
|
262
|
+
records: [
|
|
263
|
+
{ host: '_dmarc', type: 'TXT', value: 'v=DMARC1; p=reject' },
|
|
264
|
+
{ host: '@', type: 'CAA', value: '0 issue letsencrypt.org' },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`validateDmarc` preserves an exactly-one valid DMARC policy at the requested
|
|
271
|
+
host. `validateCAA` includes a requested CAA record only when that host already
|
|
272
|
+
has CAA. Both default to `false`. If every requested record is suppressed, the
|
|
273
|
+
response contains `effectiveIntent.records: []`; the UI explains that no DNS
|
|
274
|
+
changes are required and verification completes without querying DNS. The same
|
|
275
|
+
policy may be supplied at the top level of `createConnectionFlow()` and is
|
|
276
|
+
persisted on every child connection.
|
|
277
|
+
|
|
278
|
+
Every provider descriptor includes a validated HTTPS `referenceUrl`. The
|
|
279
|
+
embeddable UI opens it in a new tab with an explicit accessible label and
|
|
280
|
+
`noopener noreferrer`. This link helps users find provider-specific material;
|
|
281
|
+
it is not a claim that the provider has passed automatic conformance.
|
|
282
|
+
|
|
283
|
+
`provider.capabilities.rootNSModification` is always one of `supported`,
|
|
284
|
+
`unsupported`, or `unverified`. Domain0 exposes the assessment but never uses
|
|
285
|
+
it to resolve records in the browser. Conditional intent is resolved by the
|
|
286
|
+
backend when the provider is selected, preventing a caller from claiming a
|
|
287
|
+
capability the provider has not proved.
|
|
288
|
+
|
|
289
|
+
The framework-neutral UI exposes fully typed `onStepChange`, `onSuccess`, and
|
|
290
|
+
`onClose` lifecycle callbacks. Step events are deduplicated, success is emitted
|
|
291
|
+
once per open modal session, and close events distinguish the close button,
|
|
292
|
+
Escape, programmatic close, and destruction. The lower-level
|
|
293
|
+
`onConnectionChange` observer is revision-aware and does not repeat an
|
|
294
|
+
unchanged connection.
|
|
295
|
+
|
|
296
|
+
For an Entri-style high-level lifecycle, use the singleton `domain0` export or
|
|
297
|
+
create an isolated instance with `createDomain0()`. `connectDomain()` mounts
|
|
298
|
+
and opens the flow immediately, `close()` closes the active modal,
|
|
299
|
+
`destroy()` also removes its DOM root, and a later connection replaces the
|
|
300
|
+
previous one without leaving duplicate roots. `load()` is an asynchronous
|
|
301
|
+
preflight for API parity; the npm package is already bundled when imported.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { createDomain0Client, domain0 } from 'domain0'
|
|
305
|
+
|
|
306
|
+
await domain0.load()
|
|
307
|
+
|
|
308
|
+
const client = createDomain0Client({
|
|
309
|
+
baseUrl: 'https://domain0.example.com',
|
|
310
|
+
token: async () => obtainShortLivedConnectionToken(),
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
await domain0.connectDomain({ client, connectionId })
|
|
314
|
+
|
|
315
|
+
// Later, if the host application needs to dismiss the active flow:
|
|
316
|
+
domain0.close()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### DKIM email-provider guidance
|
|
320
|
+
|
|
321
|
+
The lower-level client exposes a connection-scoped, read-only query. The
|
|
322
|
+
backend loads the tenant-owned connection and derives its domain; callers can
|
|
323
|
+
only add bounded selectors to inspect:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { DkimSelectorSchema } from 'domain0'
|
|
327
|
+
|
|
328
|
+
const guidance = await client.getDkimGuidance(connectionId, {
|
|
329
|
+
selectors: [DkimSelectorSchema.parse('mail-2026')],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
if (guidance.detectionStatus === 'detected') {
|
|
333
|
+
console.info(guidance.provider.name, guidance.dkimDns.status)
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Set `enableDkim` to render the same guidance only after the connection becomes
|
|
338
|
+
active. Known account-specific selectors can be supplied explicitly:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
await domain0.connectDomain({
|
|
342
|
+
client,
|
|
343
|
+
connectionId,
|
|
344
|
+
enableDkim: true,
|
|
345
|
+
dkimSelectors: [DkimSelectorSchema.parse('mail-2026')],
|
|
346
|
+
onDkimSetupDocumentationClick: ({ provider }) => {
|
|
347
|
+
analytics.track('dkim_guide_opened', { provider: provider.id })
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
MX evidence is bounded to Google Workspace, Microsoft 365, and Zoho Mail.
|
|
353
|
+
Documentation URLs are a fixed discriminated union of their official guides,
|
|
354
|
+
not network-provided links. `record_observed` means only that a TXT or CNAME
|
|
355
|
+
was observed for a checked selector. `no_record_observed` is equally narrow:
|
|
356
|
+
it does not prove DKIM is disabled because selectors can differ. The UI states
|
|
357
|
+
that provider-admin status must be checked and never reports signing as enabled
|
|
358
|
+
or disabled.
|
|
359
|
+
|
|
360
|
+
### Locale and fallback contract
|
|
361
|
+
|
|
362
|
+
Every connection entry point accepts a strict `locale`. The Zod-derived
|
|
363
|
+
`Domain0Locale` contract mirrors Entri Connect's current identifiers:
|
|
364
|
+
`en`, `es`, `pt`, `pt-br`, `pt-pt`, `fr`, `it`, `de`, `nl`, `pl`, `tr`,
|
|
365
|
+
`ja`, `da`, and `sv`. The default is `en`; `pt` uses Brazilian Portuguese
|
|
366
|
+
formatting (`pt-BR`). Unknown or region-expanded values such as `en-US` are
|
|
367
|
+
rejected instead of being guessed.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
await domain0.connectDomain({
|
|
371
|
+
client,
|
|
372
|
+
connectionId,
|
|
373
|
+
locale: 'de',
|
|
374
|
+
})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Domain0 currently ships complete English, Spanish, Brazilian Portuguese,
|
|
378
|
+
European Portuguese, French, and German message catalogs. The
|
|
379
|
+
other accepted Entri identifiers use the English built-in catalog until their
|
|
380
|
+
native catalog is complete. This fallback is exposed by
|
|
381
|
+
`createDomain0Localizer(locale).translatedLocale`, so hosts and tests can
|
|
382
|
+
observe it instead of assuming a translation exists. Each mounted controller
|
|
383
|
+
owns its immutable localizer, writes the requested BCP 47 language tag and
|
|
384
|
+
direction to its root, uses `Intl` for numbers and visible dates, and preserves
|
|
385
|
+
machine-readable ISO values in native `<time datetime>` elements. Provider
|
|
386
|
+
detection explanations, plan warnings, API failures, transport failures, and
|
|
387
|
+
protocol failures are rendered from exhaustive typed mappings; raw server or
|
|
388
|
+
adapter messages remain diagnostic data and never become visible copy. The current
|
|
389
|
+
Entri locale set is left-to-right; the renderer nevertheless uses logical CSS
|
|
390
|
+
alignment so a future RTL locale does not require a layout rewrite.
|
|
391
|
+
|
|
392
|
+
### Typed browser events
|
|
393
|
+
|
|
394
|
+
Every lifecycle event is available through the strict Zod-discriminated
|
|
395
|
+
`onEvent` stream. The existing focused callbacks remain available:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
await domain0.connectDomain({
|
|
399
|
+
client,
|
|
400
|
+
connectionId,
|
|
401
|
+
onEvent: (event) => {
|
|
402
|
+
switch (event.type) {
|
|
403
|
+
case 'success':
|
|
404
|
+
case 'close':
|
|
405
|
+
case 'step_change':
|
|
406
|
+
case 'manual_setup_documentation_click':
|
|
407
|
+
case 'dkim_setup_documentation_click':
|
|
408
|
+
case 'request_close':
|
|
409
|
+
case 'shared_flow_sent':
|
|
410
|
+
analytics.track(event.type, event)
|
|
411
|
+
break
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
onRequestClose: async () => {
|
|
415
|
+
if (await confirmExit()) {
|
|
416
|
+
domain0.close()
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Providing `onRequestClose` intercepts only the in-widget Close button. The
|
|
423
|
+
modal remains open until the host closes it, and `onClose` fires only after the
|
|
424
|
+
real close. Manual-guide events fire only from the manual instructions screen.
|
|
425
|
+
|
|
426
|
+
To display the manual screen's **Copy secure setup link** action, provide a
|
|
427
|
+
`sharedFlowGateway.create` adapter that calls an application-authenticated host
|
|
428
|
+
endpoint. It returns `{ sharedFlowId, url, expiresAt }`; Domain0 validates the
|
|
429
|
+
capability-fragment URL and expiry, copies it, then emits `shared_flow_sent`.
|
|
430
|
+
Application tokens must never be placed in this browser gateway.
|
|
431
|
+
|
|
432
|
+
`manualSetupDocumentation` replaces the manual screen's generic provider guide
|
|
433
|
+
with a bounded, credential-free HTTPS application guide. It renders as a native
|
|
434
|
+
anchor with descriptive visible text, explicit new-tab wording, and
|
|
435
|
+
`noopener noreferrer`; activation still emits
|
|
436
|
+
`manual_setup_documentation_click`:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
await domain0.connectDomain({
|
|
440
|
+
client,
|
|
441
|
+
connectionId,
|
|
442
|
+
manualSetupDocumentation: 'https://docs.example.com/dns/setup',
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Set `whiteLabel.copy.manuallyScreen.disableManualSetupDocumentationLink` to
|
|
447
|
+
replace navigation with a native **Get step-by-step DNS help** action. The
|
|
448
|
+
action is rendered only when `onEvent` or
|
|
449
|
+
`onManualSetupDocumentationClick` can handle it, so the UI never presents an
|
|
450
|
+
inert link-shaped control.
|
|
451
|
+
|
|
452
|
+
### White-label theme and copy
|
|
453
|
+
|
|
454
|
+
`connectDomain()` and `loadSharedFlow()` accept the same `whiteLabel` input.
|
|
455
|
+
The exported `Domain0WhiteLabelSchema` is the runtime source of truth and
|
|
456
|
+
`Domain0WhiteLabelInput` is inferred from it. Theme values are semantic tokens,
|
|
457
|
+
not raw CSS:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
await domain0.connectDomain({
|
|
461
|
+
client,
|
|
462
|
+
connectionId,
|
|
463
|
+
applicationName: 'Example Hosting',
|
|
464
|
+
whiteLabel: {
|
|
465
|
+
logo: 'https://assets.example.com/logo.svg',
|
|
466
|
+
logoBackgroundColor: '#FFFFFF',
|
|
467
|
+
removeLogoBorder: false,
|
|
468
|
+
hideCompanyLogo: false,
|
|
469
|
+
hideCompanyName: false,
|
|
470
|
+
theme: {
|
|
471
|
+
colorMode: 'system',
|
|
472
|
+
light: {
|
|
473
|
+
primary: '#003366',
|
|
474
|
+
onPrimary: '#FFFFFF',
|
|
475
|
+
link: '#003366',
|
|
476
|
+
},
|
|
477
|
+
dark: {
|
|
478
|
+
primary: '#DBEAFE',
|
|
479
|
+
},
|
|
480
|
+
widthPx: 720,
|
|
481
|
+
dialogRadiusPx: 16,
|
|
482
|
+
buttonRadiusPx: 6,
|
|
483
|
+
inputRadiusPx: 6,
|
|
484
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
485
|
+
fontWeight: 400,
|
|
486
|
+
boldFontWeight: 650,
|
|
487
|
+
},
|
|
488
|
+
copy: {
|
|
489
|
+
initialSubtitle: {
|
|
490
|
+
en: 'Connect DNS without leaving this application.',
|
|
491
|
+
de: 'Verbinden Sie DNS, ohne diese Anwendung zu verlassen.',
|
|
492
|
+
},
|
|
493
|
+
providerLoginMessage: {
|
|
494
|
+
en: 'Continue securely with {PROVIDER}.',
|
|
495
|
+
de: 'Sicher mit {PROVIDER} fortfahren.',
|
|
496
|
+
},
|
|
497
|
+
successTitle: {
|
|
498
|
+
en: '{DOMAIN} is ready',
|
|
499
|
+
de: '{DOMAIN} ist bereit',
|
|
500
|
+
},
|
|
501
|
+
successDescription: {
|
|
502
|
+
en: 'Required DNS records for {DOMAIN} are live.',
|
|
503
|
+
de: 'Die erforderlichen DNS-Einträge für {DOMAIN} sind aktiv.',
|
|
504
|
+
},
|
|
505
|
+
successButton: { en: 'Done', de: 'Fertig' },
|
|
506
|
+
manuallyScreen: {
|
|
507
|
+
disableManualSetupDocumentationLink: false,
|
|
508
|
+
stepByStepGuide: {
|
|
509
|
+
en: 'Open our DNS setup guide',
|
|
510
|
+
de: 'Unsere DNS-Einrichtungsanleitung öffnen',
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
removeShareLogin: false,
|
|
515
|
+
skipCongratulationsScreen: false,
|
|
516
|
+
customProperties: {
|
|
517
|
+
manualConfiguration: { disableScreen: false },
|
|
518
|
+
providerLogin: {
|
|
519
|
+
forwardLink: { disable: false },
|
|
520
|
+
gotoManualLink: { disable: false },
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Each effective light and dark palette must keep body, secondary, link, error,
|
|
528
|
+
and button text at least 4.5:1 against its paired surface. Control boundaries
|
|
529
|
+
and focus indicators must reach 3:1. Colors are opaque `#RRGGBB` values;
|
|
530
|
+
the backdrop is `#RRGGBBAA`. Dimensions are bounded numbers and the font is a
|
|
531
|
+
validated local system-font stack, so the UI never evaluates caller-provided
|
|
532
|
+
CSS or fetches a font. Localized copy requires an `en` value, falls back to it
|
|
533
|
+
when the active locale is missing, is inserted only with `textContent`, rejects
|
|
534
|
+
HTML and control characters, and supports only `{PROVIDER}` or `{DOMAIN}` in
|
|
535
|
+
the fields that document those placeholders. A plain string remains a
|
|
536
|
+
backward-compatible shorthand for `{ en: value }`.
|
|
537
|
+
|
|
538
|
+
The behavior names above match Entri's current public configuration where
|
|
539
|
+
Domain0 has an equivalent safe action:
|
|
540
|
+
|
|
541
|
+
- `removeShareLogin` and `providerLogin.forwardLink.disable` suppress the
|
|
542
|
+
manual screen's shared-link handoff;
|
|
543
|
+
- `providerLogin.gotoManualLink.disable` removes entry into guided manual
|
|
544
|
+
setup and renders an explicit explanation;
|
|
545
|
+
- `manualConfiguration.disableScreen` blocks an already persisted
|
|
546
|
+
`manual_required` screen without rendering DNS records, provider guides, or
|
|
547
|
+
sharing; it does not rewrite or advance connection state; and
|
|
548
|
+
- `skipCongratulationsScreen` emits `step_change(active)` and `success`, closes
|
|
549
|
+
with reason `success`, and restores the host invoker without rendering the
|
|
550
|
+
success screen.
|
|
551
|
+
|
|
552
|
+
The combined `Domain0ConnectConfigurationSchema` rejects forced-manual mode
|
|
553
|
+
when manual entry or its screen is disabled, and rejects `enableDkim` with a
|
|
554
|
+
skipped success screen because that screen owns the DKIM guidance. Unknown
|
|
555
|
+
controls are rejected. In particular, Domain0 does not support Entri's
|
|
556
|
+
`existingRecords.disableScreen`: change-plan confirmation and the separate
|
|
557
|
+
destructive acknowledgement are never auto-confirmed.
|
|
558
|
+
|
|
559
|
+
`copy.manuallyScreen.stepByStepGuide` customizes the visible guide-link or
|
|
560
|
+
host-action label with the same strict locale-keyed English fallback as other
|
|
561
|
+
copy. Domain0 accepts plain text only and deliberately rejects Entri's embedded
|
|
562
|
+
`<link>` markup; native anchor/button semantics remain renderer-owned.
|
|
563
|
+
|
|
564
|
+
`logo` accepts only a bounded HTTPS URL without embedded credentials.
|
|
565
|
+
`hideCompanyLogo: true` prevents creation of the image element, so no logo
|
|
566
|
+
request occurs. A visible logo uses `referrerPolicy="no-referrer"`; this hides
|
|
567
|
+
the application URL but cannot hide the user's IP address or user agent from
|
|
568
|
+
the logo host. Self-host the asset on the application origin when that
|
|
569
|
+
disclosure is unacceptable. `applicationName` supplies visible company text
|
|
570
|
+
and the logo's accessible alternative when the name itself is hidden.
|
|
571
|
+
`logoBackgroundColor`, `removeLogoBorder`, `hideCompanyLogo`, and
|
|
572
|
+
`hideCompanyName` match Entri's current new-UI fields.
|
|
573
|
+
|
|
574
|
+
External font loading and native catalogs beyond English, Spanish, Brazilian
|
|
575
|
+
Portuguese, European Portuguese, French, and German remain
|
|
576
|
+
outside this contract pending their separate privacy and translation gates.
|
|
577
|
+
|
|
578
|
+
### Forced manual setup
|
|
579
|
+
|
|
580
|
+
Use the typed setup policy when the host product must always show manual DNS
|
|
581
|
+
instructions, even for a provider with an automatic adapter:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
await domain0.connectDomain({
|
|
585
|
+
client,
|
|
586
|
+
connectionId,
|
|
587
|
+
setupPolicy: { mode: 'manual' },
|
|
588
|
+
})
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Domain0 already receives the exact domain and DNS intent from the server-created
|
|
592
|
+
connection, so it does not collect a duplicate `prefilledDomain` in the browser.
|
|
593
|
+
After provider confirmation, the UI calls the existing
|
|
594
|
+
`startManualConfiguration()` use case and renders the exact record table. It
|
|
595
|
+
does not call OAuth, accept provider credentials, prepare a plan, or apply DNS.
|
|
596
|
+
The user still explicitly reports completion and Domain0 still queries every
|
|
597
|
+
authoritative nameserver before activation.
|
|
598
|
+
|
|
599
|
+
Forced manual mode affects only a connection that has not entered an automatic
|
|
600
|
+
workflow. If an existing connection is already authorizing, planning, applying,
|
|
601
|
+
or recovering from an automatic provider timeout, the UI blocks that workflow
|
|
602
|
+
and asks the user to cancel and create a new manual connection. It never rewinds
|
|
603
|
+
or reinterprets persisted connection state.
|
|
604
|
+
|
|
605
|
+
The application token remains on the integrating server. The browser lifecycle
|
|
606
|
+
receives only a connection-scoped client, or resolves an opaque shared-flow
|
|
607
|
+
fragment directly:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
await domain0.loadSharedFlow({
|
|
611
|
+
apiBaseUrl: 'https://domain0.example.com',
|
|
612
|
+
url: window.location.href,
|
|
613
|
+
})
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
For a provider whose evidence state is `domain_connect`, pass the integrating
|
|
617
|
+
application's onboarded template as `domainConnectTemplate`. The backend
|
|
618
|
+
checks support for that exact template and returns a reviewed provider handoff;
|
|
619
|
+
the UI never treats the handoff or user return as propagation success.
|
|
620
|
+
|
|
621
|
+
The package name is `domain0` and its license is Apache-2.0. Releases are
|
|
622
|
+
versioned through reviewed Changesets pull requests and published from the
|
|
623
|
+
verified GitHub Actions artifact with npm Trusted Publishing.
|
|
624
|
+
`bun run test:browser` builds the package and exercises native modal keyboard
|
|
625
|
+
behavior, dark mode, and 320 CSS-pixel reflow in pinned Chromium, Firefox, and
|
|
626
|
+
WebKit, including the high-level lifecycle; Forced Colors is emulated in
|
|
627
|
+
Chromium. Run the Domain0 repository's
|
|
628
|
+
remaining real-platform and assistive-technology checklist before treating a
|
|
629
|
+
packed SDK as accessibility-verified.
|