dkimkit 0.0.0-stage → 0.7.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 +161 -2
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +398 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/dns-host.d.ts +10 -0
- package/dist/src/dns-host.js +31 -0
- package/dist/src/dns-host.js.map +1 -0
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +51 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/hosted/api.d.ts +107 -0
- package/dist/src/hosted/api.js +919 -0
- package/dist/src/hosted/api.js.map +1 -0
- package/dist/src/hosted/auth.d.ts +80 -0
- package/dist/src/hosted/auth.js +147 -0
- package/dist/src/hosted/auth.js.map +1 -0
- package/dist/src/hosted/billing-dodo.d.ts +58 -0
- package/dist/src/hosted/billing-dodo.js +139 -0
- package/dist/src/hosted/billing-dodo.js.map +1 -0
- package/dist/src/hosted/billing-stripe.d.ts +82 -0
- package/dist/src/hosted/billing-stripe.js +205 -0
- package/dist/src/hosted/billing-stripe.js.map +1 -0
- package/dist/src/hosted/billing.d.ts +114 -0
- package/dist/src/hosted/billing.js +173 -0
- package/dist/src/hosted/billing.js.map +1 -0
- package/dist/src/hosted/bulk.d.ts +47 -0
- package/dist/src/hosted/bulk.js +66 -0
- package/dist/src/hosted/bulk.js.map +1 -0
- package/dist/src/hosted/cli.d.ts +2 -0
- package/dist/src/hosted/cli.js +318 -0
- package/dist/src/hosted/cli.js.map +1 -0
- package/dist/src/hosted/crypto.d.ts +22 -0
- package/dist/src/hosted/crypto.js +55 -0
- package/dist/src/hosted/crypto.js.map +1 -0
- package/dist/src/hosted/index.d.ts +7 -0
- package/dist/src/hosted/index.js +11 -0
- package/dist/src/hosted/index.js.map +1 -0
- package/dist/src/hosted/markdown.d.ts +16 -0
- package/dist/src/hosted/markdown.js +168 -0
- package/dist/src/hosted/markdown.js.map +1 -0
- package/dist/src/hosted/net.d.ts +9 -0
- package/dist/src/hosted/net.js +60 -0
- package/dist/src/hosted/net.js.map +1 -0
- package/dist/src/hosted/postgres-store.d.ts +62 -0
- package/dist/src/hosted/postgres-store.js +387 -0
- package/dist/src/hosted/postgres-store.js.map +1 -0
- package/dist/src/hosted/serialize.d.ts +144 -0
- package/dist/src/hosted/serialize.js +126 -0
- package/dist/src/hosted/serialize.js.map +1 -0
- package/dist/src/hosted/service.d.ts +170 -0
- package/dist/src/hosted/service.js +603 -0
- package/dist/src/hosted/service.js.map +1 -0
- package/dist/src/hosted/site.d.ts +84 -0
- package/dist/src/hosted/site.js +653 -0
- package/dist/src/hosted/site.js.map +1 -0
- package/dist/src/hosted/sqlite-store.d.ts +45 -0
- package/dist/src/hosted/sqlite-store.js +351 -0
- package/dist/src/hosted/sqlite-store.js.map +1 -0
- package/dist/src/hosted/store.d.ts +257 -0
- package/dist/src/hosted/store.js +181 -0
- package/dist/src/hosted/store.js.map +1 -0
- package/dist/src/hosted/widget.d.ts +46 -0
- package/dist/src/hosted/widget.js +130 -0
- package/dist/src/hosted/widget.js.map +1 -0
- package/dist/src/hosted/worker.d.ts +45 -0
- package/dist/src/hosted/worker.js +142 -0
- package/dist/src/hosted/worker.js.map +1 -0
- package/dist/src/http.d.ts +14 -0
- package/dist/src/http.js +54 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/orchestrator.d.ts +70 -0
- package/dist/src/orchestrator.js +69 -0
- package/dist/src/orchestrator.js.map +1 -0
- package/dist/src/propagation.d.ts +30 -0
- package/dist/src/propagation.js +76 -0
- package/dist/src/propagation.js.map +1 -0
- package/dist/src/providers/dns/aws.d.ts +28 -0
- package/dist/src/providers/dns/aws.js +103 -0
- package/dist/src/providers/dns/aws.js.map +1 -0
- package/dist/src/providers/dns/cloudflare.d.ts +26 -0
- package/dist/src/providers/dns/cloudflare.js +97 -0
- package/dist/src/providers/dns/cloudflare.js.map +1 -0
- package/dist/src/providers/dns/manual.d.ts +12 -0
- package/dist/src/providers/dns/manual.js +18 -0
- package/dist/src/providers/dns/manual.js.map +1 -0
- package/dist/src/providers/dns/route53.d.ts +34 -0
- package/dist/src/providers/dns/route53.js +176 -0
- package/dist/src/providers/dns/route53.js.map +1 -0
- package/dist/src/providers/email/factory.d.ts +29 -0
- package/dist/src/providers/email/factory.js +67 -0
- package/dist/src/providers/email/factory.js.map +1 -0
- package/dist/src/providers/email/mailgun.d.ts +36 -0
- package/dist/src/providers/email/mailgun.js +141 -0
- package/dist/src/providers/email/mailgun.js.map +1 -0
- package/dist/src/providers/email/postmark.d.ts +37 -0
- package/dist/src/providers/email/postmark.js +121 -0
- package/dist/src/providers/email/postmark.js.map +1 -0
- package/dist/src/providers/email/sendgrid.d.ts +31 -0
- package/dist/src/providers/email/sendgrid.js +109 -0
- package/dist/src/providers/email/sendgrid.js.map +1 -0
- package/dist/src/providers/email/ses.d.ts +39 -0
- package/dist/src/providers/email/ses.js +123 -0
- package/dist/src/providers/email/ses.js.map +1 -0
- package/dist/src/providers/factory.d.ts +14 -0
- package/dist/src/providers/factory.js +32 -0
- package/dist/src/providers/factory.js.map +1 -0
- package/dist/src/records.d.ts +35 -0
- package/dist/src/records.js +109 -0
- package/dist/src/records.js.map +1 -0
- package/dist/src/server.d.ts +67 -0
- package/dist/src/server.js +301 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/types.d.ts +75 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/xml.d.ts +6 -0
- package/dist/src/xml.js +23 -0
- package/dist/src/xml.js.map +1 -0
- package/docs/billing.md +43 -0
- package/docs/getting-started.md +66 -0
- package/docs/hosted-api.md +192 -0
- package/docs/library.md +253 -0
- package/docs/self-hosting.md +153 -0
- package/docs/widget.md +121 -0
- package/package.json +63 -4
- package/web/dashboard.html +669 -0
- package/web/index.html +697 -0
- package/web/widget-frame.html +480 -0
- package/web/widget.js +91 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { DnsRecord, PlannedChange, ValidationResult } from '../types.js';
|
|
2
|
+
export type DomainStatus = 'pending' | 'awaiting_dns' | 'verifying' | 'verified' | 'conflict' | 'failed';
|
|
3
|
+
export interface Account {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
apiKeyHash: string;
|
|
7
|
+
/** sendgrid | postmark | mailgun | ses */
|
|
8
|
+
emailProvider: string;
|
|
9
|
+
/** Encrypted JSON of the provider's credentials (see EmailCredentials). */
|
|
10
|
+
emailCredentialsEnc: string | null;
|
|
11
|
+
webhookUrl: string | null;
|
|
12
|
+
webhookSecret: string;
|
|
13
|
+
/** Origins allowed to embed the widget. */
|
|
14
|
+
widgetOrigins: string[];
|
|
15
|
+
/** Max domains this account may have at once; null for unlimited. Set by the operator. */
|
|
16
|
+
domainLimit: number | null;
|
|
17
|
+
/** Sign-in email for self-serve accounts; null for operator-created ones. */
|
|
18
|
+
email: string | null;
|
|
19
|
+
billingCustomerId: string | null;
|
|
20
|
+
billingSubscriptionId: string | null;
|
|
21
|
+
billingStatus: BillingStatus;
|
|
22
|
+
/** Current billing period from the payment provider, ms. */
|
|
23
|
+
billingPeriodStart: number | null;
|
|
24
|
+
billingPeriodEnd: number | null;
|
|
25
|
+
/** Active domains already reported to the payment provider this period. */
|
|
26
|
+
reportedQuantity: number;
|
|
27
|
+
/** Time (seconds) of the last billing event applied, to ignore stale, out-of-order ones. */
|
|
28
|
+
billingEventAt: number | null;
|
|
29
|
+
createdAt: number;
|
|
30
|
+
}
|
|
31
|
+
/** free: no card on file. active: paying. past_due: payment failing, the provider is retrying. */
|
|
32
|
+
export type BillingStatus = 'free' | 'active' | 'past_due';
|
|
33
|
+
/** A single-use email sign-in / sign-up link. */
|
|
34
|
+
export interface LoginToken {
|
|
35
|
+
tokenHash: string;
|
|
36
|
+
email: string;
|
|
37
|
+
name: string | null;
|
|
38
|
+
expiresAt: number;
|
|
39
|
+
usedAt: number | null;
|
|
40
|
+
createdAt: number;
|
|
41
|
+
}
|
|
42
|
+
export interface DashboardSession {
|
|
43
|
+
tokenHash: string;
|
|
44
|
+
accountId: string;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
}
|
|
48
|
+
export type UsageMetric = 'domain_created' | 'domain_verified' | 'active_domain_day';
|
|
49
|
+
/**
|
|
50
|
+
* One billable fact. Append-only, and the id is deterministic (e.g.
|
|
51
|
+
* "active_domain_day:dom_1:2026-10-25"), so recording the same fact twice is a no-op.
|
|
52
|
+
* Domain name and tenant are snapshots, so history survives domain deletion.
|
|
53
|
+
*/
|
|
54
|
+
export interface UsageRecord {
|
|
55
|
+
id: string;
|
|
56
|
+
accountId: string;
|
|
57
|
+
tenantId: string | null;
|
|
58
|
+
domainId: string;
|
|
59
|
+
domain: string;
|
|
60
|
+
metric: UsageMetric;
|
|
61
|
+
quantity: number;
|
|
62
|
+
/** UTC calendar day, YYYY-MM-DD. */
|
|
63
|
+
day: string;
|
|
64
|
+
occurredAt: number;
|
|
65
|
+
}
|
|
66
|
+
export interface UsageTotals {
|
|
67
|
+
domainsCreated: number;
|
|
68
|
+
verifications: number;
|
|
69
|
+
/** Distinct domains that were verified on at least one day of the period. */
|
|
70
|
+
activeDomains: number;
|
|
71
|
+
/** Sum over days of verified domains: for prorated billing. */
|
|
72
|
+
activeDomainDays: number;
|
|
73
|
+
}
|
|
74
|
+
export interface UsageQuery {
|
|
75
|
+
/** Inclusive UTC days, YYYY-MM-DD. */
|
|
76
|
+
from: string;
|
|
77
|
+
to: string;
|
|
78
|
+
/** Omit for every account (operator reports). */
|
|
79
|
+
accountId?: string;
|
|
80
|
+
groupByTenant?: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface UsageRow extends UsageTotals {
|
|
83
|
+
accountId: string;
|
|
84
|
+
/** Present when grouped by tenant. */
|
|
85
|
+
tenantId?: string | null;
|
|
86
|
+
}
|
|
87
|
+
export interface WidgetDefaults {
|
|
88
|
+
tenantId: string;
|
|
89
|
+
metadata: Record<string, string>;
|
|
90
|
+
subdomain: string | null;
|
|
91
|
+
sendgridSubuser: string | null;
|
|
92
|
+
/** Inbound parse webhook; the hostname becomes parse.<domain>. */
|
|
93
|
+
inboundUrl: string | null;
|
|
94
|
+
}
|
|
95
|
+
/** A short-lived browser credential scoped to one domain (or to creating one). */
|
|
96
|
+
export interface WidgetSession {
|
|
97
|
+
id: string;
|
|
98
|
+
accountId: string;
|
|
99
|
+
tokenHash: string;
|
|
100
|
+
domainId: string | null;
|
|
101
|
+
/** Used when the end user enters the domain in the widget. */
|
|
102
|
+
defaults: WidgetDefaults | null;
|
|
103
|
+
expiresAt: number;
|
|
104
|
+
createdAt: number;
|
|
105
|
+
}
|
|
106
|
+
export interface InboundConfig {
|
|
107
|
+
url: string;
|
|
108
|
+
hostname: string;
|
|
109
|
+
spamCheck: boolean;
|
|
110
|
+
sendRaw: boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface DomainRow {
|
|
113
|
+
id: string;
|
|
114
|
+
accountId: string;
|
|
115
|
+
domain: string;
|
|
116
|
+
subdomain: string | null;
|
|
117
|
+
tenantId: string | null;
|
|
118
|
+
metadata: Record<string, string>;
|
|
119
|
+
status: DomainStatus;
|
|
120
|
+
/** The domain's id at the email provider. */
|
|
121
|
+
providerDomainId: string;
|
|
122
|
+
sendgridSubuser: string | null;
|
|
123
|
+
dnsProvider: string;
|
|
124
|
+
dnsCredentialsEnc: string | null;
|
|
125
|
+
inbound: InboundConfig | null;
|
|
126
|
+
overwrite: boolean;
|
|
127
|
+
records: DnsRecord[];
|
|
128
|
+
/** Names of records not yet visible in DNS at the last check. */
|
|
129
|
+
pending: string[];
|
|
130
|
+
validation: ValidationResult | null;
|
|
131
|
+
conflicts: PlannedChange[] | null;
|
|
132
|
+
lastError: string | null;
|
|
133
|
+
attempts: number;
|
|
134
|
+
nextRunAt: number | null;
|
|
135
|
+
lockedUntil: number | null;
|
|
136
|
+
verifiedAt: number | null;
|
|
137
|
+
expiresAt: number | null;
|
|
138
|
+
createdAt: number;
|
|
139
|
+
updatedAt: number;
|
|
140
|
+
}
|
|
141
|
+
export interface DomainEvent {
|
|
142
|
+
id: string;
|
|
143
|
+
domainId: string;
|
|
144
|
+
accountId: string;
|
|
145
|
+
type: string;
|
|
146
|
+
data: Record<string, unknown>;
|
|
147
|
+
createdAt: number;
|
|
148
|
+
}
|
|
149
|
+
export interface WebhookDelivery {
|
|
150
|
+
id: string;
|
|
151
|
+
accountId: string;
|
|
152
|
+
eventId: string;
|
|
153
|
+
url: string;
|
|
154
|
+
payload: string;
|
|
155
|
+
status: 'pending' | 'delivered' | 'failed';
|
|
156
|
+
attempts: number;
|
|
157
|
+
nextAttemptAt: number | null;
|
|
158
|
+
lockedUntil: number | null;
|
|
159
|
+
lastError: string | null;
|
|
160
|
+
responseStatus: number | null;
|
|
161
|
+
createdAt: number;
|
|
162
|
+
}
|
|
163
|
+
export interface ListDomainsQuery {
|
|
164
|
+
tenantId?: string;
|
|
165
|
+
status?: DomainStatus;
|
|
166
|
+
limit: number;
|
|
167
|
+
/** Opaque cursor from a previous page. */
|
|
168
|
+
after?: string;
|
|
169
|
+
}
|
|
170
|
+
export declare class DuplicateError extends Error {
|
|
171
|
+
}
|
|
172
|
+
/** Persistence for the hosted API. Implemented by SqliteStore and PostgresStore. */
|
|
173
|
+
export interface Store {
|
|
174
|
+
createAccount(account: Account): Promise<void>;
|
|
175
|
+
getAccount(id: string): Promise<Account | undefined>;
|
|
176
|
+
getAccountByKeyHash(hash: string): Promise<Account | undefined>;
|
|
177
|
+
updateAccount(id: string, patch: Partial<Omit<Account, 'id' | 'createdAt'>>): Promise<void>;
|
|
178
|
+
/** Throws DuplicateError when the account already has this domain + subdomain. */
|
|
179
|
+
insertDomain(domain: DomainRow): Promise<void>;
|
|
180
|
+
getDomain(accountId: string, id: string): Promise<DomainRow | undefined>;
|
|
181
|
+
findDomain(accountId: string, domain: string, subdomain: string | null): Promise<DomainRow | undefined>;
|
|
182
|
+
listDomains(accountId: string, query: ListDomainsQuery): Promise<{
|
|
183
|
+
domains: DomainRow[];
|
|
184
|
+
next: string | null;
|
|
185
|
+
}>;
|
|
186
|
+
updateDomain(id: string, patch: Partial<Omit<DomainRow, 'id' | 'accountId' | 'createdAt'>>): Promise<DomainRow>;
|
|
187
|
+
deleteDomain(id: string): Promise<void>;
|
|
188
|
+
/** Atomically leases up to `limit` domains whose next run is due. */
|
|
189
|
+
claimDueDomains(now: number, limit: number, leaseMs: number): Promise<DomainRow[]>;
|
|
190
|
+
addEvent(event: DomainEvent): Promise<void>;
|
|
191
|
+
listEvents(domainId: string, limit: number): Promise<DomainEvent[]>;
|
|
192
|
+
addDelivery(delivery: WebhookDelivery): Promise<void>;
|
|
193
|
+
claimDueDeliveries(now: number, limit: number, leaseMs: number): Promise<WebhookDelivery[]>;
|
|
194
|
+
updateDelivery(id: string, patch: Partial<Omit<WebhookDelivery, 'id' | 'accountId' | 'createdAt'>>): Promise<void>;
|
|
195
|
+
createWidgetSession(session: WidgetSession): Promise<void>;
|
|
196
|
+
getWidgetSessionByHash(hash: string): Promise<WidgetSession | undefined>;
|
|
197
|
+
updateWidgetSession(id: string, patch: Partial<Pick<WidgetSession, 'domainId'>>): Promise<void>;
|
|
198
|
+
listAccounts(): Promise<Account[]>;
|
|
199
|
+
/** Current number of domains (any status) for plan limits. */
|
|
200
|
+
countDomains(accountId: string): Promise<number>;
|
|
201
|
+
/** Inserts records, silently skipping ids that already exist. */
|
|
202
|
+
recordUsage(records: UsageRecord[]): Promise<void>;
|
|
203
|
+
/** Records an active day for every currently verified domain. Idempotent per domain and day. */
|
|
204
|
+
recordActiveDomainDays(day: string, now: number): Promise<void>;
|
|
205
|
+
summarizeUsage(query: UsageQuery): Promise<UsageRow[]>;
|
|
206
|
+
getAccountByEmail(email: string): Promise<Account | undefined>;
|
|
207
|
+
getAccountByBillingCustomer(customerId: string): Promise<Account | undefined>;
|
|
208
|
+
/** Accounts with a card on file (active or past_due) and a billing customer. */
|
|
209
|
+
listBillableAccounts(): Promise<Account[]>;
|
|
210
|
+
createLoginToken(token: LoginToken): Promise<void>;
|
|
211
|
+
/** Marks the token used and returns it, or undefined if unknown, expired or already used. Atomic. */
|
|
212
|
+
consumeLoginToken(tokenHash: string, now: number): Promise<LoginToken | undefined>;
|
|
213
|
+
createDashboardSession(session: DashboardSession): Promise<void>;
|
|
214
|
+
getDashboardSession(tokenHash: string): Promise<DashboardSession | undefined>;
|
|
215
|
+
deleteDashboardSession(tokenHash: string): Promise<void>;
|
|
216
|
+
/** Records a billing webhook event id; false if it was already processed. */
|
|
217
|
+
markBillingEventProcessed(eventId: string, now: number): Promise<boolean>;
|
|
218
|
+
close(): Promise<void>;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* How a model field is stored. Each store turns these into its own encoding
|
|
222
|
+
* (e.g. JSON is TEXT in SQLite and JSONB in Postgres; booleans are 0/1 in SQLite).
|
|
223
|
+
*/
|
|
224
|
+
export type ColumnKind = 'text' | 'int' | 'bool'
|
|
225
|
+
/** '' in the column (so UNIQUE constraints work) ↔ null in the model. */
|
|
226
|
+
| 'emptyAsNull' | {
|
|
227
|
+
json: unknown;
|
|
228
|
+
};
|
|
229
|
+
export type ColumnMap<T> = Record<keyof T, readonly [column: string, kind: ColumnKind]>;
|
|
230
|
+
export declare const ACCOUNT_COLUMNS: ColumnMap<Account>;
|
|
231
|
+
export declare const LOGIN_TOKEN_COLUMNS: ColumnMap<LoginToken>;
|
|
232
|
+
export declare const DASHBOARD_SESSION_COLUMNS: ColumnMap<DashboardSession>;
|
|
233
|
+
/** Billing columns added to accounts after the first release: [column, SQLite type, Postgres type]. */
|
|
234
|
+
export declare const ACCOUNT_BILLING_COLUMNS: [string, string, string][];
|
|
235
|
+
export declare const USAGE_COLUMNS: ColumnMap<UsageRecord>;
|
|
236
|
+
/** UTC calendar day of a timestamp, YYYY-MM-DD. */
|
|
237
|
+
export declare const utcDay: (ms: number) => string;
|
|
238
|
+
/**
|
|
239
|
+
* The aggregate both stores run. Grouped by account (and tenant if asked).
|
|
240
|
+
* Parameters: $1 from, $2 to, then optionally $3 account id.
|
|
241
|
+
*/
|
|
242
|
+
export declare function usageSql(query: UsageQuery, placeholder: (n: number) => string): string;
|
|
243
|
+
/** Aggregate row → UsageRow. Counts may arrive as strings (Postgres bigint/numeric) or null. */
|
|
244
|
+
export declare function toUsageRow(row: Record<string, unknown>, groupByTenant: boolean): UsageRow;
|
|
245
|
+
export declare const DOMAIN_COLUMNS: ColumnMap<DomainRow>;
|
|
246
|
+
export declare const EVENT_COLUMNS: ColumnMap<DomainEvent>;
|
|
247
|
+
export declare const DELIVERY_COLUMNS: ColumnMap<WebhookDelivery>;
|
|
248
|
+
export declare const SESSION_COLUMNS: ColumnMap<WidgetSession>;
|
|
249
|
+
/** Model object → { column: encoded value }, skipping undefined fields (so it works for patches). */
|
|
250
|
+
export declare function toColumns<T, V>(map: ColumnMap<T>, value: Partial<T>, encode: (kind: ColumnKind, v: unknown) => V): Record<string, V>;
|
|
251
|
+
export declare function fromColumns<T>(map: ColumnMap<T>, row: Record<string, unknown> | undefined, decode: (kind: ColumnKind, v: unknown) => unknown): T | undefined;
|
|
252
|
+
/** A fresh copy of a JSON column's fallback, so rows never share one mutable object. */
|
|
253
|
+
export declare const jsonFallback: (kind: {
|
|
254
|
+
json: unknown;
|
|
255
|
+
}) => unknown;
|
|
256
|
+
/** Opens a store by URL: postgres://… or postgresql://… for Postgres, anything else is a SQLite path (or :memory:). */
|
|
257
|
+
export declare function openStore(url: string): Promise<Store>;
|
|
@@ -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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"}
|