dkimkit 0.0.0-stage → 0.6.1

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.
Files changed (128) hide show
  1. package/README.md +153 -2
  2. package/dist/src/cli.d.ts +2 -0
  3. package/dist/src/cli.js +398 -0
  4. package/dist/src/cli.js.map +1 -0
  5. package/dist/src/dns-host.d.ts +10 -0
  6. package/dist/src/dns-host.js +31 -0
  7. package/dist/src/dns-host.js.map +1 -0
  8. package/dist/src/errors.d.ts +17 -0
  9. package/dist/src/errors.js +51 -0
  10. package/dist/src/errors.js.map +1 -0
  11. package/dist/src/hosted/api.d.ts +95 -0
  12. package/dist/src/hosted/api.js +781 -0
  13. package/dist/src/hosted/api.js.map +1 -0
  14. package/dist/src/hosted/auth.d.ts +70 -0
  15. package/dist/src/hosted/auth.js +121 -0
  16. package/dist/src/hosted/auth.js.map +1 -0
  17. package/dist/src/hosted/billing-dodo.d.ts +58 -0
  18. package/dist/src/hosted/billing-dodo.js +139 -0
  19. package/dist/src/hosted/billing-dodo.js.map +1 -0
  20. package/dist/src/hosted/billing-stripe.d.ts +82 -0
  21. package/dist/src/hosted/billing-stripe.js +205 -0
  22. package/dist/src/hosted/billing-stripe.js.map +1 -0
  23. package/dist/src/hosted/billing.d.ts +114 -0
  24. package/dist/src/hosted/billing.js +173 -0
  25. package/dist/src/hosted/billing.js.map +1 -0
  26. package/dist/src/hosted/cli.d.ts +2 -0
  27. package/dist/src/hosted/cli.js +305 -0
  28. package/dist/src/hosted/cli.js.map +1 -0
  29. package/dist/src/hosted/crypto.d.ts +22 -0
  30. package/dist/src/hosted/crypto.js +55 -0
  31. package/dist/src/hosted/crypto.js.map +1 -0
  32. package/dist/src/hosted/index.d.ts +7 -0
  33. package/dist/src/hosted/index.js +11 -0
  34. package/dist/src/hosted/index.js.map +1 -0
  35. package/dist/src/hosted/markdown.d.ts +16 -0
  36. package/dist/src/hosted/markdown.js +168 -0
  37. package/dist/src/hosted/markdown.js.map +1 -0
  38. package/dist/src/hosted/net.d.ts +9 -0
  39. package/dist/src/hosted/net.js +60 -0
  40. package/dist/src/hosted/net.js.map +1 -0
  41. package/dist/src/hosted/postgres-store.d.ts +62 -0
  42. package/dist/src/hosted/postgres-store.js +387 -0
  43. package/dist/src/hosted/postgres-store.js.map +1 -0
  44. package/dist/src/hosted/serialize.d.ts +144 -0
  45. package/dist/src/hosted/serialize.js +126 -0
  46. package/dist/src/hosted/serialize.js.map +1 -0
  47. package/dist/src/hosted/service.d.ts +166 -0
  48. package/dist/src/hosted/service.js +582 -0
  49. package/dist/src/hosted/service.js.map +1 -0
  50. package/dist/src/hosted/site.d.ts +54 -0
  51. package/dist/src/hosted/site.js +472 -0
  52. package/dist/src/hosted/site.js.map +1 -0
  53. package/dist/src/hosted/sqlite-store.d.ts +45 -0
  54. package/dist/src/hosted/sqlite-store.js +351 -0
  55. package/dist/src/hosted/sqlite-store.js.map +1 -0
  56. package/dist/src/hosted/store.d.ts +257 -0
  57. package/dist/src/hosted/store.js +181 -0
  58. package/dist/src/hosted/store.js.map +1 -0
  59. package/dist/src/hosted/widget.d.ts +46 -0
  60. package/dist/src/hosted/widget.js +130 -0
  61. package/dist/src/hosted/widget.js.map +1 -0
  62. package/dist/src/hosted/worker.d.ts +45 -0
  63. package/dist/src/hosted/worker.js +142 -0
  64. package/dist/src/hosted/worker.js.map +1 -0
  65. package/dist/src/http.d.ts +14 -0
  66. package/dist/src/http.js +54 -0
  67. package/dist/src/http.js.map +1 -0
  68. package/dist/src/index.d.ts +19 -0
  69. package/dist/src/index.js +19 -0
  70. package/dist/src/index.js.map +1 -0
  71. package/dist/src/orchestrator.d.ts +70 -0
  72. package/dist/src/orchestrator.js +69 -0
  73. package/dist/src/orchestrator.js.map +1 -0
  74. package/dist/src/propagation.d.ts +30 -0
  75. package/dist/src/propagation.js +76 -0
  76. package/dist/src/propagation.js.map +1 -0
  77. package/dist/src/providers/dns/aws.d.ts +28 -0
  78. package/dist/src/providers/dns/aws.js +103 -0
  79. package/dist/src/providers/dns/aws.js.map +1 -0
  80. package/dist/src/providers/dns/cloudflare.d.ts +26 -0
  81. package/dist/src/providers/dns/cloudflare.js +97 -0
  82. package/dist/src/providers/dns/cloudflare.js.map +1 -0
  83. package/dist/src/providers/dns/manual.d.ts +12 -0
  84. package/dist/src/providers/dns/manual.js +18 -0
  85. package/dist/src/providers/dns/manual.js.map +1 -0
  86. package/dist/src/providers/dns/route53.d.ts +34 -0
  87. package/dist/src/providers/dns/route53.js +176 -0
  88. package/dist/src/providers/dns/route53.js.map +1 -0
  89. package/dist/src/providers/email/factory.d.ts +29 -0
  90. package/dist/src/providers/email/factory.js +67 -0
  91. package/dist/src/providers/email/factory.js.map +1 -0
  92. package/dist/src/providers/email/mailgun.d.ts +36 -0
  93. package/dist/src/providers/email/mailgun.js +141 -0
  94. package/dist/src/providers/email/mailgun.js.map +1 -0
  95. package/dist/src/providers/email/postmark.d.ts +37 -0
  96. package/dist/src/providers/email/postmark.js +121 -0
  97. package/dist/src/providers/email/postmark.js.map +1 -0
  98. package/dist/src/providers/email/sendgrid.d.ts +31 -0
  99. package/dist/src/providers/email/sendgrid.js +109 -0
  100. package/dist/src/providers/email/sendgrid.js.map +1 -0
  101. package/dist/src/providers/email/ses.d.ts +39 -0
  102. package/dist/src/providers/email/ses.js +123 -0
  103. package/dist/src/providers/email/ses.js.map +1 -0
  104. package/dist/src/providers/factory.d.ts +14 -0
  105. package/dist/src/providers/factory.js +32 -0
  106. package/dist/src/providers/factory.js.map +1 -0
  107. package/dist/src/records.d.ts +35 -0
  108. package/dist/src/records.js +109 -0
  109. package/dist/src/records.js.map +1 -0
  110. package/dist/src/server.d.ts +67 -0
  111. package/dist/src/server.js +301 -0
  112. package/dist/src/server.js.map +1 -0
  113. package/dist/src/types.d.ts +75 -0
  114. package/dist/src/types.js +2 -0
  115. package/dist/src/types.js.map +1 -0
  116. package/dist/src/xml.d.ts +6 -0
  117. package/dist/src/xml.js +23 -0
  118. package/dist/src/xml.js.map +1 -0
  119. package/docs/billing.md +43 -0
  120. package/docs/getting-started.md +57 -0
  121. package/docs/hosted-api.md +192 -0
  122. package/docs/self-hosting.md +148 -0
  123. package/docs/widget.md +121 -0
  124. package/package.json +63 -4
  125. package/web/dashboard.html +390 -0
  126. package/web/index.html +697 -0
  127. package/web/widget-frame.html +480 -0
  128. package/web/widget.js +91 -0
@@ -0,0 +1,181 @@
1
+ export class DuplicateError extends Error {
2
+ }
3
+ const json = (fallback = null) => ({ json: fallback });
4
+ export const ACCOUNT_COLUMNS = {
5
+ id: ['id', 'text'],
6
+ name: ['name', 'text'],
7
+ apiKeyHash: ['api_key_hash', 'text'],
8
+ emailProvider: ['email_provider', 'text'],
9
+ emailCredentialsEnc: ['email_credentials_enc', 'text'],
10
+ webhookUrl: ['webhook_url', 'text'],
11
+ webhookSecret: ['webhook_secret', 'text'],
12
+ widgetOrigins: ['widget_origins', json([])],
13
+ domainLimit: ['domain_limit', 'int'],
14
+ email: ['email', 'text'],
15
+ billingCustomerId: ['billing_customer_id', 'text'],
16
+ billingSubscriptionId: ['billing_subscription_id', 'text'],
17
+ billingStatus: ['billing_status', 'text'],
18
+ billingPeriodStart: ['billing_period_start', 'int'],
19
+ billingPeriodEnd: ['billing_period_end', 'int'],
20
+ reportedQuantity: ['reported_quantity', 'int'],
21
+ billingEventAt: ['billing_event_at', 'int'],
22
+ createdAt: ['created_at', 'int'],
23
+ };
24
+ export const LOGIN_TOKEN_COLUMNS = {
25
+ tokenHash: ['token_hash', 'text'],
26
+ email: ['email', 'text'],
27
+ name: ['name', 'text'],
28
+ expiresAt: ['expires_at', 'int'],
29
+ usedAt: ['used_at', 'int'],
30
+ createdAt: ['created_at', 'int'],
31
+ };
32
+ export const DASHBOARD_SESSION_COLUMNS = {
33
+ tokenHash: ['token_hash', 'text'],
34
+ accountId: ['account_id', 'text'],
35
+ expiresAt: ['expires_at', 'int'],
36
+ createdAt: ['created_at', 'int'],
37
+ };
38
+ /** Billing columns added to accounts after the first release: [column, SQLite type, Postgres type]. */
39
+ export const ACCOUNT_BILLING_COLUMNS = [
40
+ ['email_provider', "TEXT NOT NULL DEFAULT 'sendgrid'", "TEXT NOT NULL DEFAULT 'sendgrid'"],
41
+ ['email_credentials_enc', 'TEXT', 'TEXT'],
42
+ ['email', 'TEXT', 'TEXT'],
43
+ ['billing_customer_id', 'TEXT', 'TEXT'],
44
+ ['billing_subscription_id', 'TEXT', 'TEXT'],
45
+ ['billing_status', "TEXT NOT NULL DEFAULT 'free'", "TEXT NOT NULL DEFAULT 'free'"],
46
+ ['billing_period_start', 'INTEGER', 'BIGINT'],
47
+ ['billing_period_end', 'INTEGER', 'BIGINT'],
48
+ ['reported_quantity', 'INTEGER NOT NULL DEFAULT 0', 'INTEGER NOT NULL DEFAULT 0'],
49
+ ['billing_event_at', 'INTEGER', 'BIGINT'],
50
+ ];
51
+ export const USAGE_COLUMNS = {
52
+ id: ['id', 'text'],
53
+ accountId: ['account_id', 'text'],
54
+ tenantId: ['tenant_id', 'text'],
55
+ domainId: ['domain_id', 'text'],
56
+ domain: ['domain', 'text'],
57
+ metric: ['metric', 'text'],
58
+ quantity: ['quantity', 'int'],
59
+ day: ['day', 'text'],
60
+ occurredAt: ['occurred_at', 'int'],
61
+ };
62
+ /** UTC calendar day of a timestamp, YYYY-MM-DD. */
63
+ export const utcDay = (ms) => new Date(ms).toISOString().slice(0, 10);
64
+ /**
65
+ * The aggregate both stores run. Grouped by account (and tenant if asked).
66
+ * Parameters: $1 from, $2 to, then optionally $3 account id.
67
+ */
68
+ export function usageSql(query, placeholder) {
69
+ const groups = query.groupByTenant ? 'account_id, tenant_id' : 'account_id';
70
+ return `SELECT ${groups},
71
+ SUM(CASE WHEN metric = 'domain_created' THEN quantity ELSE 0 END) AS domains_created,
72
+ SUM(CASE WHEN metric = 'domain_verified' THEN quantity ELSE 0 END) AS verifications,
73
+ COUNT(DISTINCT CASE WHEN metric = 'active_domain_day' THEN domain_id END) AS active_domains,
74
+ SUM(CASE WHEN metric = 'active_domain_day' THEN quantity ELSE 0 END) AS active_domain_days
75
+ FROM usage_records
76
+ WHERE day >= ${placeholder(1)} AND day <= ${placeholder(2)}${query.accountId ? ` AND account_id = ${placeholder(3)}` : ''}
77
+ GROUP BY ${groups}
78
+ ORDER BY ${groups}`;
79
+ }
80
+ /** Aggregate row → UsageRow. Counts may arrive as strings (Postgres bigint/numeric) or null. */
81
+ export function toUsageRow(row, groupByTenant) {
82
+ const n = (v) => Number(v ?? 0);
83
+ return {
84
+ accountId: row.account_id,
85
+ ...(groupByTenant ? { tenantId: row.tenant_id ?? null } : {}),
86
+ domainsCreated: n(row.domains_created),
87
+ verifications: n(row.verifications),
88
+ activeDomains: n(row.active_domains),
89
+ activeDomainDays: n(row.active_domain_days),
90
+ };
91
+ }
92
+ export const DOMAIN_COLUMNS = {
93
+ id: ['id', 'text'],
94
+ accountId: ['account_id', 'text'],
95
+ domain: ['domain', 'text'],
96
+ subdomain: ['subdomain', 'emptyAsNull'],
97
+ tenantId: ['tenant_id', 'text'],
98
+ metadata: ['metadata', json({})],
99
+ status: ['status', 'text'],
100
+ // Named from when SendGrid was the only provider; kept to avoid a data migration.
101
+ providerDomainId: ['sendgrid_domain_id', 'text'],
102
+ sendgridSubuser: ['sendgrid_subuser', 'text'],
103
+ dnsProvider: ['dns_provider', 'text'],
104
+ dnsCredentialsEnc: ['dns_credentials_enc', 'text'],
105
+ inbound: ['inbound', json()],
106
+ overwrite: ['overwrite', 'bool'],
107
+ records: ['records', json([])],
108
+ pending: ['pending', json([])],
109
+ validation: ['validation', json()],
110
+ conflicts: ['conflicts', json()],
111
+ lastError: ['last_error', 'text'],
112
+ attempts: ['attempts', 'int'],
113
+ nextRunAt: ['next_run_at', 'int'],
114
+ lockedUntil: ['locked_until', 'int'],
115
+ verifiedAt: ['verified_at', 'int'],
116
+ expiresAt: ['expires_at', 'int'],
117
+ createdAt: ['created_at', 'int'],
118
+ updatedAt: ['updated_at', 'int'],
119
+ };
120
+ export const EVENT_COLUMNS = {
121
+ id: ['id', 'text'],
122
+ domainId: ['domain_id', 'text'],
123
+ accountId: ['account_id', 'text'],
124
+ type: ['type', 'text'],
125
+ data: ['data', json({})],
126
+ createdAt: ['created_at', 'int'],
127
+ };
128
+ export const DELIVERY_COLUMNS = {
129
+ id: ['id', 'text'],
130
+ accountId: ['account_id', 'text'],
131
+ eventId: ['event_id', 'text'],
132
+ url: ['url', 'text'],
133
+ payload: ['payload', 'text'],
134
+ status: ['status', 'text'],
135
+ attempts: ['attempts', 'int'],
136
+ nextAttemptAt: ['next_attempt_at', 'int'],
137
+ lockedUntil: ['locked_until', 'int'],
138
+ lastError: ['last_error', 'text'],
139
+ responseStatus: ['response_status', 'int'],
140
+ createdAt: ['created_at', 'int'],
141
+ };
142
+ export const SESSION_COLUMNS = {
143
+ id: ['id', 'text'],
144
+ accountId: ['account_id', 'text'],
145
+ tokenHash: ['token_hash', 'text'],
146
+ domainId: ['domain_id', 'text'],
147
+ defaults: ['defaults', json()],
148
+ expiresAt: ['expires_at', 'int'],
149
+ createdAt: ['created_at', 'int'],
150
+ };
151
+ /** Model object → { column: encoded value }, skipping undefined fields (so it works for patches). */
152
+ export function toColumns(map, value, encode) {
153
+ const row = {};
154
+ for (const [field, v] of Object.entries(value)) {
155
+ const entry = map[field];
156
+ if (entry && v !== undefined)
157
+ row[entry[0]] = encode(entry[1], v);
158
+ }
159
+ return row;
160
+ }
161
+ export function fromColumns(map, row, decode) {
162
+ if (!row)
163
+ return undefined;
164
+ const out = {};
165
+ for (const [field, [column, kind]] of Object.entries(map)) {
166
+ out[field] = decode(kind, row[column]);
167
+ }
168
+ return out;
169
+ }
170
+ /** A fresh copy of a JSON column's fallback, so rows never share one mutable object. */
171
+ export const jsonFallback = (kind) => structuredClone(kind.json);
172
+ /** Opens a store by URL: postgres://… or postgresql://… for Postgres, anything else is a SQLite path (or :memory:). */
173
+ export async function openStore(url) {
174
+ if (/^postgres(ql)?:\/\//.test(url)) {
175
+ const { PostgresStore } = await import('./postgres-store.js');
176
+ return PostgresStore.connect({ connectionString: url });
177
+ }
178
+ const { SqliteStore } = await import('./sqlite-store.js');
179
+ return new SqliteStore(url.replace(/^sqlite:/, ''));
180
+ }
181
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/hosted/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type DnsHost } from '../dns-host.js';
2
+ import { type DnsCredentials, type DomainService } from './service.js';
3
+ import type { Account, DomainRow, Store, WidgetDefaults, WidgetSession } from './store.js';
4
+ export declare const newSessionToken: () => string;
5
+ export declare const MAX_SESSION_SEC: number;
6
+ export interface WidgetContext {
7
+ session: WidgetSession;
8
+ account: Account;
9
+ }
10
+ export interface WidgetOptions {
11
+ now?: () => number;
12
+ detectHost?: (domain: string) => Promise<DnsHost>;
13
+ }
14
+ /**
15
+ * Backs the embeddable widget. Everything is scoped to one session, which the
16
+ * app's backend mints with its secret key; the browser only ever holds the
17
+ * short-lived session token.
18
+ */
19
+ export declare class WidgetService {
20
+ private readonly domains;
21
+ private readonly store;
22
+ private readonly now;
23
+ private readonly detectHost;
24
+ private readonly hostCache;
25
+ constructor(domains: DomainService, store: Store, opts?: WidgetOptions);
26
+ createSession(account: Account, input: {
27
+ domainId?: string;
28
+ defaults?: WidgetDefaults;
29
+ expiresInSec?: number;
30
+ }): Promise<{
31
+ session: WidgetSession;
32
+ token: string;
33
+ }>;
34
+ authenticate(token: string): Promise<WidgetContext | undefined>;
35
+ domainOf(ctx: WidgetContext): Promise<DomainRow | null>;
36
+ /** Where the domain's DNS is hosted, so the widget can suggest the right setup path. */
37
+ hostOf(domain: string): Promise<DnsHost>;
38
+ /** For sessions created without a domain: the end user enters it. */
39
+ setDomain(ctx: WidgetContext, input: string): Promise<DomainRow>;
40
+ /** Switches how DNS gets written. Credentials are checked against the zone before they're saved. */
41
+ connect(ctx: WidgetContext, provider: string, credentials: DnsCredentials): Promise<DomainRow>;
42
+ verify(ctx: WidgetContext): Promise<DomainRow>;
43
+ /** The end user approves replacing their existing records that conflict. */
44
+ resolveConflicts(ctx: WidgetContext): Promise<DomainRow>;
45
+ private requireDomain;
46
+ }
@@ -0,0 +1,130 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { detectDnsHost } from '../dns-host.js';
3
+ import { ApiError, apiErrorHint, ConfigError } from '../errors.js';
4
+ import { isValidDomain, normalizeName } from '../records.js';
5
+ import { hashApiKey, newId } from './crypto.js';
6
+ import { ConflictError, PlanLimitError } from './service.js';
7
+ export const newSessionToken = () => `wss_${randomBytes(24).toString('base64url')}`;
8
+ export const MAX_SESSION_SEC = 24 * 3600;
9
+ const DEFAULT_SESSION_SEC = 3600;
10
+ const HOST_CACHE_MS = 10 * 60e3;
11
+ /**
12
+ * Backs the embeddable widget. Everything is scoped to one session, which the
13
+ * app's backend mints with its secret key; the browser only ever holds the
14
+ * short-lived session token.
15
+ */
16
+ export class WidgetService {
17
+ domains;
18
+ store;
19
+ now;
20
+ detectHost;
21
+ hostCache = new Map();
22
+ constructor(domains, store, opts = {}) {
23
+ this.domains = domains;
24
+ this.store = store;
25
+ this.now = opts.now ?? Date.now;
26
+ this.detectHost = opts.detectHost ?? ((domain) => detectDnsHost(domain));
27
+ }
28
+ async createSession(account, input) {
29
+ if (account.widgetOrigins.length === 0) {
30
+ throw new ConfigError('Set widget_origins on your account first (PATCH /v1/account), e.g. ["https://app.example.com"].');
31
+ }
32
+ if (input.domainId)
33
+ await this.domains.getDomain(account, input.domainId);
34
+ else if (!input.defaults)
35
+ throw new ConfigError('Pass domain_id, or tenant_id to let the user enter their own domain.');
36
+ const token = newSessionToken();
37
+ const now = this.now();
38
+ const session = {
39
+ id: newId('ws'),
40
+ accountId: account.id,
41
+ tokenHash: hashApiKey(token),
42
+ domainId: input.domainId ?? null,
43
+ defaults: input.domainId ? null : input.defaults,
44
+ expiresAt: now + (input.expiresInSec ?? DEFAULT_SESSION_SEC) * 1000,
45
+ createdAt: now,
46
+ };
47
+ await this.store.createWidgetSession(session);
48
+ return { session, token };
49
+ }
50
+ async authenticate(token) {
51
+ const session = await this.store.getWidgetSessionByHash(hashApiKey(token));
52
+ if (!session || session.expiresAt <= this.now())
53
+ return undefined;
54
+ const account = await this.store.getAccount(session.accountId);
55
+ return account ? { session, account } : undefined;
56
+ }
57
+ async domainOf(ctx) {
58
+ return ctx.session.domainId ? ((await this.store.getDomain(ctx.account.id, ctx.session.domainId)) ?? null) : null;
59
+ }
60
+ /** Where the domain's DNS is hosted, so the widget can suggest the right setup path. */
61
+ async hostOf(domain) {
62
+ const cached = this.hostCache.get(domain);
63
+ if (cached && this.now() - cached.at < HOST_CACHE_MS)
64
+ return cached.host;
65
+ const host = await this.detectHost(domain);
66
+ this.hostCache.set(domain, { host, at: this.now() });
67
+ return host;
68
+ }
69
+ /** For sessions created without a domain: the end user enters it. */
70
+ async setDomain(ctx, input) {
71
+ const defaults = ctx.session.defaults;
72
+ if (ctx.session.domainId || !defaults)
73
+ throw new ConfigError('This session already has a domain.');
74
+ const domain = normalizeName(input);
75
+ if (!isValidDomain(domain))
76
+ throw new ConfigError('Enter a domain like example.com.');
77
+ let row = await this.store.findDomain(ctx.account.id, domain, defaults.subdomain);
78
+ if (row && row.tenantId !== defaults.tenantId) {
79
+ throw new ConflictError(`${domain} is already set up by someone else. Contact support if it's yours.`, '');
80
+ }
81
+ // The app owner's plan limit is their business; end users get a neutral message.
82
+ row ??= await this.domains.createDomain(ctx.account, {
83
+ domain,
84
+ subdomain: defaults.subdomain,
85
+ tenantId: defaults.tenantId,
86
+ metadata: defaults.metadata,
87
+ sendgridSubuser: defaults.sendgridSubuser,
88
+ // The email provider picks the inbound hostname (e.g. parse.<domain>).
89
+ inbound: defaults.inboundUrl ? { url: defaults.inboundUrl, spamCheck: false, sendRaw: false } : null,
90
+ dnsProvider: 'manual',
91
+ }).catch((err) => {
92
+ if (err instanceof PlanLimitError) {
93
+ throw new ConfigError(`Adding a domain isn't available right now. Please contact ${ctx.account.name} support.`);
94
+ }
95
+ throw err;
96
+ });
97
+ await this.store.updateWidgetSession(ctx.session.id, { domainId: row.id });
98
+ ctx.session.domainId = row.id;
99
+ return row;
100
+ }
101
+ /** Switches how DNS gets written. Credentials are checked against the zone before they're saved. */
102
+ async connect(ctx, provider, credentials) {
103
+ const row = await this.requireDomain(ctx);
104
+ try {
105
+ await this.domains.testDnsAccess(row, provider, credentials);
106
+ }
107
+ catch (err) {
108
+ if (err instanceof ApiError && err.status < 500) {
109
+ const detail = err.message.replace(/^.*?\(\d+\):\s*/, '');
110
+ throw new ConfigError(`${err.provider} didn't accept these credentials: ${detail}. ${apiErrorHint(err) ?? ''}`.trim());
111
+ }
112
+ throw err;
113
+ }
114
+ return this.domains.updateDomain(ctx.account, row.id, { dnsProvider: provider, dnsCredentials: credentials });
115
+ }
116
+ async verify(ctx) {
117
+ return this.domains.verifyNow(ctx.account, (await this.requireDomain(ctx)).id);
118
+ }
119
+ /** The end user approves replacing their existing records that conflict. */
120
+ async resolveConflicts(ctx) {
121
+ return this.domains.updateDomain(ctx.account, (await this.requireDomain(ctx)).id, { overwrite: true });
122
+ }
123
+ async requireDomain(ctx) {
124
+ const row = await this.domainOf(ctx);
125
+ if (!row)
126
+ throw new ConfigError('Enter your domain first.');
127
+ return row;
128
+ }
129
+ }
130
+ //# sourceMappingURL=widget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget.js","sourceRoot":"","sources":["../../../src/hosted/widget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { FetchLike } from '../http.js';
2
+ import type { DomainService, Logger } from './service.js';
3
+ import type { Store } from './store.js';
4
+ export interface RunnerOptions {
5
+ /** Domains checked in parallel. Default 4. */
6
+ concurrency?: number;
7
+ pollMs?: number;
8
+ /** How long a claimed job stays locked if the process dies mid-run. */
9
+ leaseMs?: number;
10
+ now?: () => number;
11
+ fetch?: FetchLike;
12
+ allowPrivateWebhooks?: boolean;
13
+ /** Reports usage to the billing provider after each metering pass. */
14
+ billing?: {
15
+ reportUsage(): Promise<void>;
16
+ };
17
+ log?: Logger;
18
+ }
19
+ /** Polls the store for due domain checks and webhook deliveries. Safe to run in several processes. */
20
+ export declare class BackgroundRunner {
21
+ private readonly service;
22
+ private readonly store;
23
+ private readonly opts;
24
+ private readonly concurrency;
25
+ private readonly pollMs;
26
+ private readonly leaseMs;
27
+ private readonly now;
28
+ private readonly fetchImpl;
29
+ private readonly log;
30
+ private readonly inflight;
31
+ private domainsRunning;
32
+ private lastMeteredAt;
33
+ private timer;
34
+ private stopped;
35
+ constructor(service: DomainService, store: Store, opts?: RunnerOptions);
36
+ start(): void;
37
+ /** Stops polling and waits for in-flight work to finish. */
38
+ stop(): Promise<void>;
39
+ /** Claims and starts whatever is due. Doesn't wait for the work itself; see `drain`. */
40
+ tick(): Promise<void>;
41
+ drain(): Promise<void>;
42
+ private track;
43
+ private deliver;
44
+ private retryLater;
45
+ }
@@ -0,0 +1,142 @@
1
+ import { signWebhook, WEBHOOK_SIGNATURE_HEADER } from './crypto.js';
2
+ import { assertPublicHttpsUrl } from './net.js';
3
+ // After the first attempt: 10s, 1m, 5m, 30m, 2h, 6h, 12h, 24h — then give up.
4
+ const WEBHOOK_RETRY_MS = [10e3, 60e3, 5 * 60e3, 30 * 60e3, 2 * 3600e3, 6 * 3600e3, 12 * 3600e3, 24 * 3600e3];
5
+ const WEBHOOK_TIMEOUT_MS = 10_000;
6
+ const METER_EVERY_MS = 3600e3;
7
+ /** Polls the store for due domain checks and webhook deliveries. Safe to run in several processes. */
8
+ export class BackgroundRunner {
9
+ service;
10
+ store;
11
+ opts;
12
+ concurrency;
13
+ pollMs;
14
+ leaseMs;
15
+ now;
16
+ fetchImpl;
17
+ log;
18
+ inflight = new Set();
19
+ domainsRunning = 0;
20
+ lastMeteredAt = -Infinity;
21
+ timer;
22
+ stopped = true;
23
+ constructor(service, store, opts = {}) {
24
+ this.service = service;
25
+ this.store = store;
26
+ this.opts = opts;
27
+ this.concurrency = opts.concurrency ?? 4;
28
+ this.pollMs = opts.pollMs ?? 2000;
29
+ this.leaseMs = opts.leaseMs ?? 5 * 60e3;
30
+ this.now = opts.now ?? Date.now;
31
+ this.fetchImpl = opts.fetch ?? fetch;
32
+ this.log = opts.log ?? (() => { });
33
+ }
34
+ start() {
35
+ this.stopped = false;
36
+ const loop = async () => {
37
+ if (this.stopped)
38
+ return;
39
+ try {
40
+ await this.tick();
41
+ }
42
+ catch (err) {
43
+ // e.g. the database is briefly unreachable; try again next poll.
44
+ this.log('error', 'runner tick failed', { error: String(err) });
45
+ }
46
+ if (!this.stopped)
47
+ this.timer = setTimeout(loop, this.pollMs);
48
+ };
49
+ void loop();
50
+ }
51
+ /** Stops polling and waits for in-flight work to finish. */
52
+ async stop() {
53
+ this.stopped = true;
54
+ clearTimeout(this.timer);
55
+ await this.drain();
56
+ }
57
+ /** Claims and starts whatever is due. Doesn't wait for the work itself; see `drain`. */
58
+ async tick() {
59
+ const now = this.now();
60
+ const runs = [];
61
+ const free = this.concurrency - this.domainsRunning;
62
+ if (free > 0) {
63
+ for (const row of await this.store.claimDueDomains(now, free, this.leaseMs)) {
64
+ this.domainsRunning++;
65
+ const run = this.service
66
+ .runDomain(row)
67
+ .catch((err) => this.log('error', 'domain run crashed', { domain_id: row.id, error: String(err) }))
68
+ .finally(() => this.domainsRunning--);
69
+ runs.push(run);
70
+ this.track(run);
71
+ }
72
+ }
73
+ for (const delivery of await this.store.claimDueDeliveries(now, 10, this.leaseMs)) {
74
+ this.track(this.deliver(delivery));
75
+ }
76
+ // Usage metering: an active day per verified domain. Idempotent, so several
77
+ // runner processes doing this at once is fine.
78
+ if (now - this.lastMeteredAt >= METER_EVERY_MS) {
79
+ this.lastMeteredAt = now;
80
+ const meter = async () => {
81
+ // Count domains this pass just verified, not only those verified before it.
82
+ await Promise.allSettled(runs);
83
+ await this.service.meterActiveDomains();
84
+ // After metering, so the report includes today's active days.
85
+ await this.opts.billing?.reportUsage();
86
+ };
87
+ this.track(meter().catch((err) => this.log('error', 'usage metering failed', { error: String(err) })));
88
+ }
89
+ }
90
+ async drain() {
91
+ while (this.inflight.size > 0)
92
+ await Promise.allSettled([...this.inflight]);
93
+ }
94
+ track(p) {
95
+ this.inflight.add(p);
96
+ void p.finally(() => this.inflight.delete(p));
97
+ }
98
+ async deliver(d) {
99
+ const account = await this.store.getAccount(d.accountId);
100
+ if (!account)
101
+ return;
102
+ const attempts = d.attempts + 1;
103
+ try {
104
+ await assertPublicHttpsUrl(d.url, { allowPrivate: this.opts.allowPrivateWebhooks });
105
+ const event = JSON.parse(d.payload);
106
+ const res = await this.fetchImpl(d.url, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'content-type': 'application/json',
110
+ 'user-agent': 'dkimkit-webhooks/1',
111
+ [WEBHOOK_SIGNATURE_HEADER]: signWebhook(account.webhookSecret, d.payload, Math.floor(this.now() / 1000)),
112
+ 'x-dkimkit-event-id': event.id,
113
+ 'x-dkimkit-event-type': event.type,
114
+ },
115
+ body: d.payload,
116
+ redirect: 'manual',
117
+ signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),
118
+ });
119
+ await res.body?.cancel();
120
+ if (res.ok) {
121
+ await this.store.updateDelivery(d.id, { status: 'delivered', attempts, responseStatus: res.status, lastError: null, lockedUntil: null, nextAttemptAt: null });
122
+ return;
123
+ }
124
+ await this.retryLater(d, attempts, `HTTP ${res.status}`, res.status);
125
+ }
126
+ catch (err) {
127
+ await this.retryLater(d, attempts, err instanceof Error ? err.message : String(err), null);
128
+ }
129
+ }
130
+ async retryLater(d, attempts, error, status) {
131
+ const delay = WEBHOOK_RETRY_MS[attempts - 1];
132
+ if (delay === undefined) {
133
+ await this.store.updateDelivery(d.id, { status: 'failed', attempts, lastError: error, responseStatus: status, lockedUntil: null, nextAttemptAt: null });
134
+ this.log('warn', 'webhook delivery abandoned', { delivery_id: d.id, error });
135
+ return;
136
+ }
137
+ await this.store.updateDelivery(d.id, {
138
+ attempts, lastError: error, responseStatus: status, lockedUntil: null, nextAttemptAt: this.now() + delay,
139
+ });
140
+ }
141
+ }
142
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src/hosted/worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
2
+ export interface RequestOptions {
3
+ method?: string;
4
+ headers?: Record<string, string>;
5
+ body?: unknown;
6
+ }
7
+ export declare const sleep: (ms: number, signal?: AbortSignal) => Promise<undefined>;
8
+ /** JSON request with retries on rate limits and transient server errors. */
9
+ export declare function requestJson<T>(fetchImpl: FetchLike, provider: string, url: string, opts: RequestOptions, describeError: (body: unknown) => string | undefined, retries?: number): Promise<T>;
10
+ /**
11
+ * Raw request returning the response text. `buildInit` runs per attempt so
12
+ * callers can re-sign requests on retry.
13
+ */
14
+ export declare function requestText(fetchImpl: FetchLike, provider: string, url: string, buildInit: () => RequestInit, describeError: (text: string) => string | undefined, retries?: number): Promise<string>;
@@ -0,0 +1,54 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { ApiError } from './errors.js';
3
+ const RETRYABLE = new Set([429, 500, 502, 503, 504]);
4
+ const MAX_RETRY_DELAY_MS = 30_000;
5
+ export const sleep = (ms, signal) => delay(ms, undefined, { signal });
6
+ /** JSON request with retries on rate limits and transient server errors. */
7
+ export async function requestJson(fetchImpl, provider, url, opts, describeError, retries = 2) {
8
+ const hasBody = opts.body !== undefined;
9
+ const init = {
10
+ method: opts.method ?? 'GET',
11
+ headers: {
12
+ Accept: 'application/json',
13
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
14
+ ...opts.headers,
15
+ },
16
+ body: hasBody ? JSON.stringify(opts.body) : undefined,
17
+ };
18
+ const text = await send(fetchImpl, provider, url, () => init, (t) => describeError(parseJson(t)), retries);
19
+ return (text ? parseJson(text) : undefined);
20
+ }
21
+ /**
22
+ * Raw request returning the response text. `buildInit` runs per attempt so
23
+ * callers can re-sign requests on retry.
24
+ */
25
+ export async function requestText(fetchImpl, provider, url, buildInit, describeError, retries = 2) {
26
+ return send(fetchImpl, provider, url, buildInit, describeError, retries);
27
+ }
28
+ async function send(fetchImpl, provider, url, buildInit, describeError, retries) {
29
+ for (let attempt = 0;; attempt++) {
30
+ const res = await fetchImpl(url, buildInit());
31
+ const text = await res.text();
32
+ if (res.ok)
33
+ return text;
34
+ if (RETRYABLE.has(res.status) && attempt < retries) {
35
+ await sleep(retryDelay(res, attempt));
36
+ continue;
37
+ }
38
+ throw new ApiError(provider, res.status, describeError(text) || res.statusText || `HTTP ${res.status}`, parseJson(text));
39
+ }
40
+ }
41
+ function parseJson(text) {
42
+ try {
43
+ return JSON.parse(text);
44
+ }
45
+ catch {
46
+ return text;
47
+ }
48
+ }
49
+ function retryDelay(res, attempt) {
50
+ const retryAfter = Number(res.headers.get('retry-after'));
51
+ const ms = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 1000 * 2 ** attempt;
52
+ return Math.min(ms, MAX_RETRY_DELAY_MS);
53
+ }
54
+ //# sourceMappingURL=http.js.map