@hasna/domains 0.0.28 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/dns.d.ts.map +1 -1
- package/dist/cli/commands/domain.d.ts.map +1 -1
- package/dist/cli/commands/outreach.d.ts.map +1 -1
- package/dist/cli/commands/owner.d.ts.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/ssl.d.ts.map +1 -1
- package/dist/cli/index.js +8262 -1400
- package/dist/cli/tui/App.d.ts.map +1 -1
- package/dist/cli/tui/DomainDetail.d.ts +1 -1
- package/dist/cli/tui/DomainDetail.d.ts.map +1 -1
- package/dist/db/dns-tools.d.ts +10 -6
- package/dist/db/dns-tools.d.ts.map +1 -1
- package/dist/db/domain-owners.d.ts +6 -21
- package/dist/db/domain-owners.d.ts.map +1 -1
- package/dist/db/domain-reputation.d.ts +0 -11
- package/dist/db/domain-reputation.d.ts.map +1 -1
- package/dist/db/domain-research.d.ts +1 -1
- package/dist/db/domain-research.d.ts.map +1 -1
- package/dist/db/domains.d.ts +51 -8
- package/dist/db/domains.d.ts.map +1 -1
- package/dist/db/history.d.ts +27 -0
- package/dist/db/history.d.ts.map +1 -0
- package/dist/db/monitoring.d.ts +2 -2
- package/dist/db/monitoring.d.ts.map +1 -1
- package/dist/db/owners.d.ts +48 -0
- package/dist/db/owners.d.ts.map +1 -0
- package/dist/db/reputation.d.ts +28 -0
- package/dist/db/reputation.d.ts.map +1 -0
- package/dist/db/store.d.ts +257 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13312 -5984
- package/dist/lib/brandsight.d.ts +3 -3
- package/dist/lib/brandsight.d.ts.map +1 -1
- package/dist/lib/cloudflare.js +3 -3
- package/dist/lib/godaddy.d.ts +3 -3
- package/dist/lib/godaddy.d.ts.map +1 -1
- package/dist/lib/namecheap.d.ts +5 -5
- package/dist/lib/namecheap.d.ts.map +1 -1
- package/dist/lib/registrar.d.ts +4 -4
- package/dist/lib/registrar.d.ts.map +1 -1
- package/dist/lib/registrar.js +17 -17
- package/dist/lib/route53.js +3 -3
- package/dist/lib/sedo.d.ts +1 -1
- package/dist/lib/sedo.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +8 -3
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +9107 -6859
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/index.js +1014 -10
- package/dist/server/repo.d.ts +53 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/package.json +4 -7
- package/dist/cli/commands/domains.d.ts +0 -3
- package/dist/cli/commands/domains.d.ts.map +0 -1
- package/dist/cli/commands/storage.d.ts +0 -3
- package/dist/cli/commands/storage.d.ts.map +0 -1
- package/dist/db/remote-storage.d.ts +0 -10
- package/dist/db/remote-storage.d.ts.map +0 -1
- package/dist/db/storage-sync.d.ts +0 -55
- package/dist/db/storage-sync.d.ts.map +0 -1
- package/dist/mcp/storage-tools.d.ts +0 -3
- package/dist/mcp/storage-tools.d.ts.map +0 -1
- package/dist/storage.d.ts +0 -5
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -825
package/dist/storage.js
DELETED
|
@@ -1,825 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
function __accessProp(key) {
|
|
8
|
-
return this[key];
|
|
9
|
-
}
|
|
10
|
-
var __toESMCache_node;
|
|
11
|
-
var __toESMCache_esm;
|
|
12
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
-
var canCache = mod != null && typeof mod === "object";
|
|
14
|
-
if (canCache) {
|
|
15
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
-
var cached = cache.get(mod);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached;
|
|
19
|
-
}
|
|
20
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
-
for (let key of __getOwnPropNames(mod))
|
|
23
|
-
if (!__hasOwnProp.call(to, key))
|
|
24
|
-
__defProp(to, key, {
|
|
25
|
-
get: __accessProp.bind(mod, key),
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
if (canCache)
|
|
29
|
-
cache.set(mod, to);
|
|
30
|
-
return to;
|
|
31
|
-
};
|
|
32
|
-
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
-
var __returnValue = (v) => v;
|
|
34
|
-
function __exportSetter(name, newValue) {
|
|
35
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
36
|
-
}
|
|
37
|
-
var __export = (target, all) => {
|
|
38
|
-
for (var name in all)
|
|
39
|
-
__defProp(target, name, {
|
|
40
|
-
get: all[name],
|
|
41
|
-
enumerable: true,
|
|
42
|
-
configurable: true,
|
|
43
|
-
set: __exportSetter.bind(all, name)
|
|
44
|
-
});
|
|
45
|
-
};
|
|
46
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
|
-
var __require = import.meta.require;
|
|
48
|
-
|
|
49
|
-
// src/db/pg-migrations.ts
|
|
50
|
-
var PG_MIGRATIONS = [
|
|
51
|
-
`CREATE TABLE IF NOT EXISTS domains (
|
|
52
|
-
id TEXT PRIMARY KEY,
|
|
53
|
-
name TEXT NOT NULL UNIQUE,
|
|
54
|
-
registrar TEXT,
|
|
55
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('discovered', 'researching', 'offered', 'negotiating', 'purchased', 'active', 'not_available', 'premium_only', 'declined', 'expired', 'transferring', 'redemption')),
|
|
56
|
-
registered_at TEXT,
|
|
57
|
-
expires_at TEXT,
|
|
58
|
-
auto_renew BOOLEAN NOT NULL DEFAULT TRUE,
|
|
59
|
-
is_premium BOOLEAN NOT NULL DEFAULT FALSE,
|
|
60
|
-
premium_price DOUBLE PRECISION,
|
|
61
|
-
standard_price DOUBLE PRECISION,
|
|
62
|
-
purchase_price DOUBLE PRECISION,
|
|
63
|
-
purchase_date TEXT,
|
|
64
|
-
nameservers TEXT NOT NULL DEFAULT '[]',
|
|
65
|
-
whois TEXT NOT NULL DEFAULT '{}',
|
|
66
|
-
ssl_expires_at TEXT,
|
|
67
|
-
ssl_issuer TEXT,
|
|
68
|
-
notes TEXT,
|
|
69
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
70
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text,
|
|
71
|
-
updated_at TEXT NOT NULL DEFAULT NOW()::text
|
|
72
|
-
)`,
|
|
73
|
-
`CREATE TABLE IF NOT EXISTS dns_records (
|
|
74
|
-
id TEXT PRIMARY KEY,
|
|
75
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
76
|
-
type TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV')),
|
|
77
|
-
name TEXT NOT NULL,
|
|
78
|
-
value TEXT NOT NULL,
|
|
79
|
-
ttl INTEGER NOT NULL DEFAULT 3600,
|
|
80
|
-
priority INTEGER,
|
|
81
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
82
|
-
)`,
|
|
83
|
-
`CREATE TABLE IF NOT EXISTS alerts (
|
|
84
|
-
id TEXT PRIMARY KEY,
|
|
85
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
86
|
-
type TEXT NOT NULL CHECK (type IN ('expiry', 'ssl_expiry', 'dns_change')),
|
|
87
|
-
trigger_days_before INTEGER,
|
|
88
|
-
sent_at TEXT,
|
|
89
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
90
|
-
)`,
|
|
91
|
-
`CREATE TABLE IF NOT EXISTS domain_offers (
|
|
92
|
-
id TEXT PRIMARY KEY,
|
|
93
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
94
|
-
our_offer DOUBLE PRECISION,
|
|
95
|
-
their_ask DOUBLE PRECISION,
|
|
96
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'countered')),
|
|
97
|
-
notes TEXT,
|
|
98
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text,
|
|
99
|
-
owner_contact_id TEXT
|
|
100
|
-
)`,
|
|
101
|
-
`CREATE TABLE IF NOT EXISTS domain_emails (
|
|
102
|
-
id TEXT PRIMARY KEY,
|
|
103
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
104
|
-
email_id TEXT NOT NULL,
|
|
105
|
-
thread_id TEXT,
|
|
106
|
-
type TEXT NOT NULL CHECK (type IN ('inquiry', 'offer', 'counter_offer', 'confirmation', 'renewal_notice', 'transfer')),
|
|
107
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text,
|
|
108
|
-
UNIQUE(domain_id, email_id)
|
|
109
|
-
)`,
|
|
110
|
-
`CREATE TABLE IF NOT EXISTS domain_owners (
|
|
111
|
-
id TEXT PRIMARY KEY,
|
|
112
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
113
|
-
contact_id TEXT,
|
|
114
|
-
owner_name TEXT,
|
|
115
|
-
owner_email TEXT,
|
|
116
|
-
owner_phone TEXT,
|
|
117
|
-
owner_organization TEXT,
|
|
118
|
-
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('whois', 'manual', 'brandsight', 'import')),
|
|
119
|
-
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
120
|
-
notes TEXT,
|
|
121
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text,
|
|
122
|
-
updated_at TEXT NOT NULL DEFAULT NOW()::text
|
|
123
|
-
)`,
|
|
124
|
-
`CREATE TABLE IF NOT EXISTS domain_history (
|
|
125
|
-
id TEXT PRIMARY KEY,
|
|
126
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
127
|
-
snapshot_type TEXT NOT NULL CHECK (snapshot_type IN ('whois', 'rdap', 'dns', 'ssl', 'reputation', 'exa_research', 'purchase', 'renewal')),
|
|
128
|
-
raw_data TEXT NOT NULL DEFAULT '{}',
|
|
129
|
-
registrant_name TEXT,
|
|
130
|
-
registrant_email TEXT,
|
|
131
|
-
registrant_org TEXT,
|
|
132
|
-
nameservers TEXT NOT NULL DEFAULT '[]',
|
|
133
|
-
registrar TEXT,
|
|
134
|
-
status TEXT,
|
|
135
|
-
notes TEXT,
|
|
136
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
137
|
-
)`,
|
|
138
|
-
`CREATE TABLE IF NOT EXISTS domain_reputation (
|
|
139
|
-
id TEXT PRIMARY KEY,
|
|
140
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
141
|
-
is_blacklisted BOOLEAN NOT NULL DEFAULT FALSE,
|
|
142
|
-
blacklist_sources TEXT NOT NULL DEFAULT '[]',
|
|
143
|
-
threat_score INTEGER,
|
|
144
|
-
spam_score INTEGER,
|
|
145
|
-
malware_detected BOOLEAN NOT NULL DEFAULT FALSE,
|
|
146
|
-
phishing_detected BOOLEAN NOT NULL DEFAULT FALSE,
|
|
147
|
-
reputation_sources TEXT NOT NULL DEFAULT '[]',
|
|
148
|
-
last_checked_at TEXT,
|
|
149
|
-
notes TEXT,
|
|
150
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text,
|
|
151
|
-
updated_at TEXT NOT NULL DEFAULT NOW()::text
|
|
152
|
-
)`,
|
|
153
|
-
`CREATE INDEX IF NOT EXISTS idx_domains_name ON domains(name)`,
|
|
154
|
-
`CREATE INDEX IF NOT EXISTS idx_domains_registrar ON domains(registrar)`,
|
|
155
|
-
`CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status)`,
|
|
156
|
-
`CREATE INDEX IF NOT EXISTS idx_domains_expires_at ON domains(expires_at)`,
|
|
157
|
-
`CREATE INDEX IF NOT EXISTS idx_domains_is_premium ON domains(is_premium)`,
|
|
158
|
-
`CREATE INDEX IF NOT EXISTS idx_dns_records_domain ON dns_records(domain_id)`,
|
|
159
|
-
`CREATE INDEX IF NOT EXISTS idx_dns_records_type ON dns_records(type)`,
|
|
160
|
-
`CREATE INDEX IF NOT EXISTS idx_alerts_domain ON alerts(domain_id)`,
|
|
161
|
-
`CREATE INDEX IF NOT EXISTS idx_alerts_type ON alerts(type)`,
|
|
162
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_offers_domain ON domain_offers(domain_id)`,
|
|
163
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_offers_owner ON domain_offers(owner_contact_id)`,
|
|
164
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_emails_domain ON domain_emails(domain_id)`,
|
|
165
|
-
`CREATE UNIQUE INDEX IF NOT EXISTS idx_domain_emails_unique ON domain_emails(domain_id, email_id)`,
|
|
166
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_owners_domain ON domain_owners(domain_id)`,
|
|
167
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_owners_contact ON domain_owners(contact_id)`,
|
|
168
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_owners_email ON domain_owners(owner_email)`,
|
|
169
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_history_domain ON domain_history(domain_id)`,
|
|
170
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_history_type ON domain_history(snapshot_type)`,
|
|
171
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_history_created ON domain_history(created_at)`,
|
|
172
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_history_email ON domain_history(registrant_email)`,
|
|
173
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_reputation_domain ON domain_reputation(domain_id)`,
|
|
174
|
-
`CREATE INDEX IF NOT EXISTS idx_domain_reputation_blacklisted ON domain_reputation(is_blacklisted)`
|
|
175
|
-
];
|
|
176
|
-
// src/db/remote-storage.ts
|
|
177
|
-
import pg from "pg";
|
|
178
|
-
function translatePlaceholders(sql) {
|
|
179
|
-
let index = 0;
|
|
180
|
-
return sql.replace(/\?/g, () => `$${++index}`);
|
|
181
|
-
}
|
|
182
|
-
function normalizeParams(params) {
|
|
183
|
-
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
184
|
-
return flat.map((value) => value === undefined ? null : value);
|
|
185
|
-
}
|
|
186
|
-
function sslConfigFor(connectionString) {
|
|
187
|
-
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
class PgAdapterAsync {
|
|
191
|
-
pool;
|
|
192
|
-
constructor(connectionString) {
|
|
193
|
-
this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
|
|
194
|
-
}
|
|
195
|
-
async run(sql, ...params) {
|
|
196
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
197
|
-
return { changes: result.rowCount ?? 0 };
|
|
198
|
-
}
|
|
199
|
-
async all(sql, ...params) {
|
|
200
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
201
|
-
return result.rows;
|
|
202
|
-
}
|
|
203
|
-
async close() {
|
|
204
|
-
await this.pool.end();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
// src/db/database.ts
|
|
208
|
-
import { Database } from "bun:sqlite";
|
|
209
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
210
|
-
import { dirname, join, resolve } from "path";
|
|
211
|
-
import { homedir } from "os";
|
|
212
|
-
|
|
213
|
-
// src/db/migrations.ts
|
|
214
|
-
var MIGRATIONS = [
|
|
215
|
-
{
|
|
216
|
-
id: 1,
|
|
217
|
-
name: "initial_schema",
|
|
218
|
-
sql: `
|
|
219
|
-
CREATE TABLE IF NOT EXISTS domains (
|
|
220
|
-
id TEXT PRIMARY KEY,
|
|
221
|
-
name TEXT NOT NULL UNIQUE,
|
|
222
|
-
registrar TEXT,
|
|
223
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'transferring', 'redemption')),
|
|
224
|
-
registered_at TEXT,
|
|
225
|
-
expires_at TEXT,
|
|
226
|
-
auto_renew INTEGER NOT NULL DEFAULT 1,
|
|
227
|
-
nameservers TEXT NOT NULL DEFAULT '[]',
|
|
228
|
-
whois TEXT NOT NULL DEFAULT '{}',
|
|
229
|
-
ssl_expires_at TEXT,
|
|
230
|
-
ssl_issuer TEXT,
|
|
231
|
-
notes TEXT,
|
|
232
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
233
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
234
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
CREATE TABLE IF NOT EXISTS dns_records (
|
|
238
|
-
id TEXT PRIMARY KEY,
|
|
239
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
240
|
-
type TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV')),
|
|
241
|
-
name TEXT NOT NULL,
|
|
242
|
-
value TEXT NOT NULL,
|
|
243
|
-
ttl INTEGER NOT NULL DEFAULT 3600,
|
|
244
|
-
priority INTEGER,
|
|
245
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
CREATE TABLE IF NOT EXISTS alerts (
|
|
249
|
-
id TEXT PRIMARY KEY,
|
|
250
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
251
|
-
type TEXT NOT NULL CHECK (type IN ('expiry', 'ssl_expiry', 'dns_change')),
|
|
252
|
-
trigger_days_before INTEGER,
|
|
253
|
-
sent_at TEXT,
|
|
254
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
CREATE INDEX IF NOT EXISTS idx_domains_name ON domains(name);
|
|
258
|
-
CREATE INDEX IF NOT EXISTS idx_domains_registrar ON domains(registrar);
|
|
259
|
-
CREATE INDEX IF NOT EXISTS idx_domains_status ON domains(status);
|
|
260
|
-
CREATE INDEX IF NOT EXISTS idx_domains_expires_at ON domains(expires_at);
|
|
261
|
-
CREATE INDEX IF NOT EXISTS idx_dns_records_domain ON dns_records(domain_id);
|
|
262
|
-
CREATE INDEX IF NOT EXISTS idx_dns_records_type ON dns_records(type);
|
|
263
|
-
CREATE INDEX IF NOT EXISTS idx_alerts_domain ON alerts(domain_id);
|
|
264
|
-
CREATE INDEX IF NOT EXISTS idx_alerts_type ON alerts(type);
|
|
265
|
-
`
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: 2,
|
|
269
|
-
name: "domain_purchase_tracking",
|
|
270
|
-
sql: `
|
|
271
|
-
PRAGMA foreign_keys=OFF;
|
|
272
|
-
|
|
273
|
-
ALTER TABLE dns_records RENAME TO dns_records_old;
|
|
274
|
-
ALTER TABLE alerts RENAME TO alerts_old;
|
|
275
|
-
ALTER TABLE domains RENAME TO domains_old;
|
|
276
|
-
|
|
277
|
-
CREATE TABLE domains (
|
|
278
|
-
id TEXT PRIMARY KEY,
|
|
279
|
-
name TEXT NOT NULL UNIQUE,
|
|
280
|
-
registrar TEXT,
|
|
281
|
-
status TEXT NOT NULL DEFAULT 'active' CHECK (
|
|
282
|
-
status IN (
|
|
283
|
-
'discovered',
|
|
284
|
-
'researching',
|
|
285
|
-
'offered',
|
|
286
|
-
'negotiating',
|
|
287
|
-
'purchased',
|
|
288
|
-
'active',
|
|
289
|
-
'not_available',
|
|
290
|
-
'premium_only',
|
|
291
|
-
'declined',
|
|
292
|
-
'expired',
|
|
293
|
-
'transferring',
|
|
294
|
-
'redemption'
|
|
295
|
-
)
|
|
296
|
-
),
|
|
297
|
-
registered_at TEXT,
|
|
298
|
-
expires_at TEXT,
|
|
299
|
-
auto_renew INTEGER NOT NULL DEFAULT 1,
|
|
300
|
-
is_premium INTEGER NOT NULL DEFAULT 0,
|
|
301
|
-
premium_price REAL,
|
|
302
|
-
standard_price REAL,
|
|
303
|
-
purchase_price REAL,
|
|
304
|
-
purchase_date TEXT,
|
|
305
|
-
nameservers TEXT NOT NULL DEFAULT '[]',
|
|
306
|
-
whois TEXT NOT NULL DEFAULT '{}',
|
|
307
|
-
ssl_expires_at TEXT,
|
|
308
|
-
ssl_issuer TEXT,
|
|
309
|
-
notes TEXT,
|
|
310
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
311
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
312
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
CREATE TABLE dns_records (
|
|
316
|
-
id TEXT PRIMARY KEY,
|
|
317
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
318
|
-
type TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV')),
|
|
319
|
-
name TEXT NOT NULL,
|
|
320
|
-
value TEXT NOT NULL,
|
|
321
|
-
ttl INTEGER NOT NULL DEFAULT 3600,
|
|
322
|
-
priority INTEGER,
|
|
323
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
CREATE TABLE alerts (
|
|
327
|
-
id TEXT PRIMARY KEY,
|
|
328
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
329
|
-
type TEXT NOT NULL CHECK (type IN ('expiry', 'ssl_expiry', 'dns_change')),
|
|
330
|
-
trigger_days_before INTEGER,
|
|
331
|
-
sent_at TEXT,
|
|
332
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
INSERT INTO domains (
|
|
336
|
-
id, name, registrar, status, registered_at, expires_at, auto_renew,
|
|
337
|
-
is_premium, premium_price, standard_price, purchase_price, purchase_date,
|
|
338
|
-
nameservers, whois, ssl_expires_at, ssl_issuer, notes, metadata, created_at, updated_at
|
|
339
|
-
)
|
|
340
|
-
SELECT
|
|
341
|
-
id, name, registrar, status, registered_at, expires_at, auto_renew,
|
|
342
|
-
0, NULL, NULL, NULL, NULL,
|
|
343
|
-
nameservers, whois, ssl_expires_at, ssl_issuer, notes, metadata, created_at, updated_at
|
|
344
|
-
FROM domains_old;
|
|
345
|
-
|
|
346
|
-
INSERT INTO dns_records (id, domain_id, type, name, value, ttl, priority, created_at)
|
|
347
|
-
SELECT id, domain_id, type, name, value, ttl, priority, created_at
|
|
348
|
-
FROM dns_records_old;
|
|
349
|
-
|
|
350
|
-
INSERT INTO alerts (id, domain_id, type, trigger_days_before, sent_at, created_at)
|
|
351
|
-
SELECT id, domain_id, type, trigger_days_before, sent_at, created_at
|
|
352
|
-
FROM alerts_old;
|
|
353
|
-
|
|
354
|
-
DROP TABLE domains_old;
|
|
355
|
-
DROP TABLE dns_records_old;
|
|
356
|
-
DROP TABLE alerts_old;
|
|
357
|
-
|
|
358
|
-
CREATE TABLE domain_offers (
|
|
359
|
-
id TEXT PRIMARY KEY,
|
|
360
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
361
|
-
our_offer REAL,
|
|
362
|
-
their_ask REAL,
|
|
363
|
-
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'countered')),
|
|
364
|
-
notes TEXT,
|
|
365
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
CREATE TABLE domain_emails (
|
|
369
|
-
id TEXT PRIMARY KEY,
|
|
370
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
371
|
-
email_id TEXT NOT NULL,
|
|
372
|
-
thread_id TEXT,
|
|
373
|
-
type TEXT NOT NULL CHECK (type IN ('inquiry', 'offer', 'counter_offer', 'confirmation', 'renewal_notice', 'transfer')),
|
|
374
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
CREATE INDEX idx_domains_name ON domains(name);
|
|
378
|
-
CREATE INDEX idx_domains_registrar ON domains(registrar);
|
|
379
|
-
CREATE INDEX idx_domains_status ON domains(status);
|
|
380
|
-
CREATE INDEX idx_domains_expires_at ON domains(expires_at);
|
|
381
|
-
CREATE INDEX idx_domains_is_premium ON domains(is_premium);
|
|
382
|
-
CREATE INDEX idx_dns_records_domain ON dns_records(domain_id);
|
|
383
|
-
CREATE INDEX idx_dns_records_type ON dns_records(type);
|
|
384
|
-
CREATE INDEX idx_alerts_domain ON alerts(domain_id);
|
|
385
|
-
CREATE INDEX idx_alerts_type ON alerts(type);
|
|
386
|
-
CREATE INDEX idx_domain_offers_domain ON domain_offers(domain_id);
|
|
387
|
-
CREATE INDEX idx_domain_emails_domain ON domain_emails(domain_id);
|
|
388
|
-
CREATE UNIQUE INDEX idx_domain_emails_unique ON domain_emails(domain_id, email_id);
|
|
389
|
-
|
|
390
|
-
PRAGMA foreign_keys=ON;
|
|
391
|
-
`
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
id: 3,
|
|
395
|
-
name: "domain_owner_tracking",
|
|
396
|
-
sql: `
|
|
397
|
-
CREATE TABLE IF NOT EXISTS domain_owners (
|
|
398
|
-
id TEXT PRIMARY KEY,
|
|
399
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
400
|
-
contact_id TEXT,
|
|
401
|
-
owner_name TEXT,
|
|
402
|
-
owner_email TEXT,
|
|
403
|
-
owner_phone TEXT,
|
|
404
|
-
owner_organization TEXT,
|
|
405
|
-
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('whois', 'manual', 'brandsight', 'import')),
|
|
406
|
-
verified INTEGER NOT NULL DEFAULT 0,
|
|
407
|
-
notes TEXT,
|
|
408
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
409
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
ALTER TABLE domain_offers ADD COLUMN owner_contact_id TEXT REFERENCES domain_owners(id);
|
|
413
|
-
|
|
414
|
-
CREATE INDEX idx_domain_owners_domain ON domain_owners(domain_id);
|
|
415
|
-
CREATE INDEX idx_domain_owners_contact ON domain_owners(contact_id);
|
|
416
|
-
CREATE INDEX idx_domain_owners_email ON domain_owners(owner_email);
|
|
417
|
-
CREATE INDEX idx_domain_offers_owner ON domain_offers(owner_contact_id);
|
|
418
|
-
`
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
id: 4,
|
|
422
|
-
name: "domain_history_tracking",
|
|
423
|
-
sql: `
|
|
424
|
-
CREATE TABLE IF NOT EXISTS domain_history (
|
|
425
|
-
id TEXT PRIMARY KEY,
|
|
426
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
427
|
-
snapshot_type TEXT NOT NULL CHECK (snapshot_type IN ('whois', 'rdap', 'dns', 'ssl', 'reputation', 'exa_research')),
|
|
428
|
-
raw_data TEXT NOT NULL DEFAULT '{}',
|
|
429
|
-
registrant_name TEXT,
|
|
430
|
-
registrant_email TEXT,
|
|
431
|
-
registrant_org TEXT,
|
|
432
|
-
nameservers TEXT NOT NULL DEFAULT '[]',
|
|
433
|
-
registrar TEXT,
|
|
434
|
-
status TEXT,
|
|
435
|
-
notes TEXT,
|
|
436
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
CREATE INDEX idx_domain_history_domain ON domain_history(domain_id);
|
|
440
|
-
CREATE INDEX idx_domain_history_type ON domain_history(snapshot_type);
|
|
441
|
-
CREATE INDEX idx_domain_history_created ON domain_history(created_at);
|
|
442
|
-
CREATE INDEX idx_domain_history_email ON domain_history(registrant_email);
|
|
443
|
-
`
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
id: 5,
|
|
447
|
-
name: "domain_reputation_tracking",
|
|
448
|
-
sql: `
|
|
449
|
-
CREATE TABLE IF NOT EXISTS domain_reputation (
|
|
450
|
-
id TEXT PRIMARY KEY,
|
|
451
|
-
domain_id TEXT NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
|
452
|
-
is_blacklisted INTEGER NOT NULL DEFAULT 0,
|
|
453
|
-
blacklist_sources TEXT NOT NULL DEFAULT '[]',
|
|
454
|
-
threat_score INTEGER,
|
|
455
|
-
spam_score INTEGER,
|
|
456
|
-
malware_detected INTEGER NOT NULL DEFAULT 0,
|
|
457
|
-
phishing_detected INTEGER NOT NULL DEFAULT 0,
|
|
458
|
-
reputation_sources TEXT NOT NULL DEFAULT '[]',
|
|
459
|
-
last_checked_at TEXT,
|
|
460
|
-
notes TEXT,
|
|
461
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
462
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
CREATE INDEX idx_domain_reputation_domain ON domain_reputation(domain_id);
|
|
466
|
-
CREATE INDEX idx_domain_reputation_blacklisted ON domain_reputation(is_blacklisted);
|
|
467
|
-
`
|
|
468
|
-
}
|
|
469
|
-
];
|
|
470
|
-
|
|
471
|
-
// src/db/database.ts
|
|
472
|
-
var _db = null;
|
|
473
|
-
function getDbPath() {
|
|
474
|
-
if (process.env["DOMAINS_DB_PATH"])
|
|
475
|
-
return process.env["DOMAINS_DB_PATH"];
|
|
476
|
-
if (process.env["HASNA_DOMAINS_DB_PATH"])
|
|
477
|
-
return process.env["HASNA_DOMAINS_DB_PATH"];
|
|
478
|
-
const explicit = process.env["DOMAINS_DIR"] ?? process.env["HASNA_DOMAINS_DIR"];
|
|
479
|
-
if (explicit) {
|
|
480
|
-
return join(explicit, "domains.db");
|
|
481
|
-
}
|
|
482
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
483
|
-
const legacyDir = join(home, ".hasna", "domains");
|
|
484
|
-
if (existsSync(legacyDir))
|
|
485
|
-
return join(legacyDir, "domains.db");
|
|
486
|
-
const xdgData = process.env["XDG_DATA_HOME"] || join(home, ".local", "share");
|
|
487
|
-
const newDir = join(xdgData, "open-domains");
|
|
488
|
-
migrateDotfile("domains", newDir);
|
|
489
|
-
return join(newDir, "domains.db");
|
|
490
|
-
}
|
|
491
|
-
function migrateDotfile(name, newDir) {
|
|
492
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
493
|
-
const oldDir = join(home, `.${name}`);
|
|
494
|
-
if (!existsSync(oldDir) || existsSync(newDir))
|
|
495
|
-
return;
|
|
496
|
-
mkdirSync(newDir, { recursive: true });
|
|
497
|
-
for (const file of readdirSync(oldDir)) {
|
|
498
|
-
const oldPath = join(oldDir, file);
|
|
499
|
-
if (statSync(oldPath).isFile())
|
|
500
|
-
copyFileSync(oldPath, join(newDir, file));
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
function getDatabase() {
|
|
504
|
-
if (_db)
|
|
505
|
-
return _db;
|
|
506
|
-
const dbPath = getDbPath();
|
|
507
|
-
if (dbPath !== ":memory:") {
|
|
508
|
-
const dir = dirname(resolve(dbPath));
|
|
509
|
-
mkdirSync(dir, { recursive: true });
|
|
510
|
-
}
|
|
511
|
-
_db = new Database(dbPath);
|
|
512
|
-
_db.exec("PRAGMA journal_mode = WAL");
|
|
513
|
-
_db.exec("PRAGMA foreign_keys = ON");
|
|
514
|
-
_db.exec(`
|
|
515
|
-
CREATE TABLE IF NOT EXISTS _migrations (
|
|
516
|
-
id INTEGER PRIMARY KEY,
|
|
517
|
-
name TEXT NOT NULL,
|
|
518
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
519
|
-
)
|
|
520
|
-
`);
|
|
521
|
-
const applied = _db.query("SELECT id FROM _migrations ORDER BY id").all();
|
|
522
|
-
const appliedIds = new Set(applied.map((r) => r.id));
|
|
523
|
-
for (const migration of MIGRATIONS) {
|
|
524
|
-
if (appliedIds.has(migration.id))
|
|
525
|
-
continue;
|
|
526
|
-
_db.exec("BEGIN");
|
|
527
|
-
try {
|
|
528
|
-
_db.exec(migration.sql);
|
|
529
|
-
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(migration.id, migration.name);
|
|
530
|
-
_db.exec("COMMIT");
|
|
531
|
-
} catch (error) {
|
|
532
|
-
_db.exec("ROLLBACK");
|
|
533
|
-
throw new Error(`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return _db;
|
|
537
|
-
}
|
|
538
|
-
function closeDatabase() {
|
|
539
|
-
if (_db) {
|
|
540
|
-
_db.close();
|
|
541
|
-
_db = null;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/db/storage-sync.ts
|
|
546
|
-
var STORAGE_TABLES = [
|
|
547
|
-
"domains",
|
|
548
|
-
"dns_records",
|
|
549
|
-
"alerts",
|
|
550
|
-
"domain_offers",
|
|
551
|
-
"domain_emails",
|
|
552
|
-
"domain_owners",
|
|
553
|
-
"domain_history",
|
|
554
|
-
"domain_reputation"
|
|
555
|
-
];
|
|
556
|
-
var DOMAINS_STORAGE_TABLES = STORAGE_TABLES;
|
|
557
|
-
var PRIMARY_KEYS = {
|
|
558
|
-
domains: ["id"],
|
|
559
|
-
dns_records: ["id"],
|
|
560
|
-
alerts: ["id"],
|
|
561
|
-
domain_offers: ["id"],
|
|
562
|
-
domain_emails: ["id"],
|
|
563
|
-
domain_owners: ["id"],
|
|
564
|
-
domain_history: ["id"],
|
|
565
|
-
domain_reputation: ["id"]
|
|
566
|
-
};
|
|
567
|
-
var DOMAINS_STORAGE_ENV = "DOMAINS_DATABASE_URL";
|
|
568
|
-
var DOMAINS_STORAGE_FALLBACK_ENV = "HASNA_DOMAINS_DATABASE_URL";
|
|
569
|
-
var DOMAINS_STORAGE_MODE_ENV = "DOMAINS_STORAGE_MODE";
|
|
570
|
-
var DOMAINS_STORAGE_MODE_FALLBACK_ENV = "HASNA_DOMAINS_STORAGE_MODE";
|
|
571
|
-
var STORAGE_DATABASE_ENV = [DOMAINS_STORAGE_ENV, DOMAINS_STORAGE_FALLBACK_ENV];
|
|
572
|
-
var STORAGE_MODE_ENV = [DOMAINS_STORAGE_MODE_ENV, DOMAINS_STORAGE_MODE_FALLBACK_ENV];
|
|
573
|
-
function firstEnv(names) {
|
|
574
|
-
for (const name of names) {
|
|
575
|
-
const value = process.env[name];
|
|
576
|
-
if (value)
|
|
577
|
-
return value;
|
|
578
|
-
}
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
function normalizeStorageMode(value) {
|
|
582
|
-
if (!value)
|
|
583
|
-
return null;
|
|
584
|
-
const normalized = value.trim().toLowerCase();
|
|
585
|
-
if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
|
|
586
|
-
return normalized;
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
function getStorageDatabaseUrl() {
|
|
590
|
-
return firstEnv(STORAGE_DATABASE_ENV);
|
|
591
|
-
}
|
|
592
|
-
function getStorageDatabaseEnvName() {
|
|
593
|
-
for (const name of STORAGE_DATABASE_ENV) {
|
|
594
|
-
if (process.env[name])
|
|
595
|
-
return name;
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
function getStorageMode() {
|
|
600
|
-
return normalizeStorageMode(firstEnv(STORAGE_MODE_ENV)) ?? (getStorageDatabaseUrl() ? "remote" : "local");
|
|
601
|
-
}
|
|
602
|
-
async function getStoragePg() {
|
|
603
|
-
const url = getStorageDatabaseUrl();
|
|
604
|
-
if (!url) {
|
|
605
|
-
throw new Error("Missing DOMAINS_DATABASE_URL or HASNA_DOMAINS_DATABASE_URL");
|
|
606
|
-
}
|
|
607
|
-
return new PgAdapterAsync(url);
|
|
608
|
-
}
|
|
609
|
-
async function runStorageMigrations(remote) {
|
|
610
|
-
for (const sql of PG_MIGRATIONS)
|
|
611
|
-
await remote.run(sql);
|
|
612
|
-
}
|
|
613
|
-
async function storagePush(options) {
|
|
614
|
-
const remote = await getStoragePg();
|
|
615
|
-
const db = getDatabase();
|
|
616
|
-
try {
|
|
617
|
-
await runStorageMigrations(remote);
|
|
618
|
-
const results = [];
|
|
619
|
-
for (const table of resolveTables(options?.tables))
|
|
620
|
-
results.push(await pushTable(db, remote, table));
|
|
621
|
-
recordSyncMeta(db, "push", results);
|
|
622
|
-
return results;
|
|
623
|
-
} finally {
|
|
624
|
-
await remote.close();
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
async function storagePull(options) {
|
|
628
|
-
const remote = await getStoragePg();
|
|
629
|
-
const db = getDatabase();
|
|
630
|
-
try {
|
|
631
|
-
await runStorageMigrations(remote);
|
|
632
|
-
const results = [];
|
|
633
|
-
for (const table of resolveTables(options?.tables))
|
|
634
|
-
results.push(await pullTable(remote, db, table));
|
|
635
|
-
recordSyncMeta(db, "pull", results);
|
|
636
|
-
return results;
|
|
637
|
-
} finally {
|
|
638
|
-
await remote.close();
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
async function storageSync(options) {
|
|
642
|
-
const pull = await storagePull(options);
|
|
643
|
-
const push = await storagePush(options);
|
|
644
|
-
return { pull, push };
|
|
645
|
-
}
|
|
646
|
-
function getStorageSyncMetaAll() {
|
|
647
|
-
const db = getDatabase();
|
|
648
|
-
ensureSyncMetaTable(db);
|
|
649
|
-
return db.query("SELECT table_name, last_synced_at, direction FROM _domains_sync_meta ORDER BY table_name, direction").all();
|
|
650
|
-
}
|
|
651
|
-
function getStorageStatus() {
|
|
652
|
-
return {
|
|
653
|
-
configured: Boolean(getStorageDatabaseUrl()),
|
|
654
|
-
env: STORAGE_DATABASE_ENV,
|
|
655
|
-
mode: getStorageMode(),
|
|
656
|
-
service: "domains",
|
|
657
|
-
tables: STORAGE_TABLES,
|
|
658
|
-
sync: getStorageSyncMetaAll()
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
function getSyncMetaAll() {
|
|
662
|
-
return getStorageSyncMetaAll();
|
|
663
|
-
}
|
|
664
|
-
function resolveTables(tables) {
|
|
665
|
-
if (!tables || tables.length === 0)
|
|
666
|
-
return [...STORAGE_TABLES];
|
|
667
|
-
const allowed = new Set(STORAGE_TABLES);
|
|
668
|
-
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
669
|
-
const invalid = requested.filter((table) => !allowed.has(table));
|
|
670
|
-
if (invalid.length > 0)
|
|
671
|
-
throw new Error(`Unknown domains sync table(s): ${invalid.join(", ")}`);
|
|
672
|
-
return requested;
|
|
673
|
-
}
|
|
674
|
-
async function pushTable(db, remote, table) {
|
|
675
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
676
|
-
try {
|
|
677
|
-
if (!sqliteTableExists(db, table))
|
|
678
|
-
return result;
|
|
679
|
-
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
680
|
-
result.rowsRead = rows.length;
|
|
681
|
-
if (rows.length === 0)
|
|
682
|
-
return result;
|
|
683
|
-
const remoteColumns = await getRemoteColumns(remote, table);
|
|
684
|
-
const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
|
|
685
|
-
result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
|
|
686
|
-
} catch (error) {
|
|
687
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
688
|
-
}
|
|
689
|
-
return result;
|
|
690
|
-
}
|
|
691
|
-
async function pullTable(remote, db, table) {
|
|
692
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
693
|
-
try {
|
|
694
|
-
if (!sqliteTableExists(db, table))
|
|
695
|
-
return result;
|
|
696
|
-
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
697
|
-
result.rowsRead = rows.length;
|
|
698
|
-
if (rows.length === 0)
|
|
699
|
-
return result;
|
|
700
|
-
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
701
|
-
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
702
|
-
} catch (error) {
|
|
703
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
704
|
-
}
|
|
705
|
-
return result;
|
|
706
|
-
}
|
|
707
|
-
async function getRemoteColumns(remote, table) {
|
|
708
|
-
const rows = await remote.all("SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
|
|
709
|
-
return new Map(rows.map((row) => [row.column_name, row.data_type]));
|
|
710
|
-
}
|
|
711
|
-
function filterRemoteColumns(remoteColumns, columns) {
|
|
712
|
-
if (remoteColumns.size === 0)
|
|
713
|
-
return columns;
|
|
714
|
-
return columns.filter((column) => remoteColumns.has(column));
|
|
715
|
-
}
|
|
716
|
-
function filterLocalColumns(db, table, columns) {
|
|
717
|
-
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
718
|
-
const allowed = new Set(rows.map((row) => row.name));
|
|
719
|
-
return columns.filter((column) => allowed.has(column));
|
|
720
|
-
}
|
|
721
|
-
async function upsertPg(remote, table, columns, rows, remoteColumns) {
|
|
722
|
-
if (columns.length === 0)
|
|
723
|
-
return 0;
|
|
724
|
-
const primaryKeys = PRIMARY_KEYS[table];
|
|
725
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
726
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
727
|
-
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
728
|
-
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
729
|
-
const fallbackKey = primaryKeys[0];
|
|
730
|
-
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
731
|
-
for (const row of rows) {
|
|
732
|
-
await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column], remoteColumns.get(column))));
|
|
733
|
-
}
|
|
734
|
-
return rows.length;
|
|
735
|
-
}
|
|
736
|
-
function upsertSqlite(db, table, columns, rows) {
|
|
737
|
-
if (columns.length === 0)
|
|
738
|
-
return 0;
|
|
739
|
-
const primaryKeys = PRIMARY_KEYS[table];
|
|
740
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
741
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
742
|
-
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
743
|
-
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
744
|
-
const fallbackKey = primaryKeys[0];
|
|
745
|
-
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
|
|
746
|
-
const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
|
|
747
|
-
for (const row of rows)
|
|
748
|
-
statement.run(...columns.map((column) => coerceForSqlite(row[column])));
|
|
749
|
-
return rows.length;
|
|
750
|
-
}
|
|
751
|
-
function recordSyncMeta(db, direction, results) {
|
|
752
|
-
ensureSyncMetaTable(db);
|
|
753
|
-
const timestamp = new Date().toISOString();
|
|
754
|
-
const statement = db.prepare("INSERT INTO _domains_sync_meta (table_name, last_synced_at, direction) VALUES (?, ?, ?) ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at");
|
|
755
|
-
for (const result of results) {
|
|
756
|
-
if (result.errors.length > 0)
|
|
757
|
-
continue;
|
|
758
|
-
statement.run(result.table, timestamp, direction);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
function ensureSyncMetaTable(db) {
|
|
762
|
-
db.exec("CREATE TABLE IF NOT EXISTS _domains_sync_meta (table_name TEXT NOT NULL, last_synced_at TEXT, direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')), PRIMARY KEY (table_name, direction))");
|
|
763
|
-
}
|
|
764
|
-
function sqliteTableExists(db, table) {
|
|
765
|
-
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table));
|
|
766
|
-
}
|
|
767
|
-
function quoteIdent(identifier) {
|
|
768
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
769
|
-
}
|
|
770
|
-
function coerceForPg(value, dataType) {
|
|
771
|
-
if (value === undefined || value === null)
|
|
772
|
-
return null;
|
|
773
|
-
if (dataType === "boolean") {
|
|
774
|
-
if (typeof value === "boolean")
|
|
775
|
-
return value;
|
|
776
|
-
if (typeof value === "number")
|
|
777
|
-
return value !== 0;
|
|
778
|
-
if (typeof value === "string")
|
|
779
|
-
return value === "1" || value.toLowerCase() === "true";
|
|
780
|
-
}
|
|
781
|
-
if (value instanceof Date)
|
|
782
|
-
return value.toISOString();
|
|
783
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
784
|
-
return value;
|
|
785
|
-
if (typeof value === "object")
|
|
786
|
-
return JSON.stringify(value);
|
|
787
|
-
return value;
|
|
788
|
-
}
|
|
789
|
-
function coerceForSqlite(value) {
|
|
790
|
-
if (value === undefined || value === null)
|
|
791
|
-
return null;
|
|
792
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
793
|
-
return value;
|
|
794
|
-
if (value instanceof Date)
|
|
795
|
-
return value.toISOString();
|
|
796
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
797
|
-
return value;
|
|
798
|
-
if (typeof value === "object")
|
|
799
|
-
return JSON.stringify(value);
|
|
800
|
-
return String(value);
|
|
801
|
-
}
|
|
802
|
-
export {
|
|
803
|
-
storageSync,
|
|
804
|
-
storagePush,
|
|
805
|
-
storagePull,
|
|
806
|
-
runStorageMigrations,
|
|
807
|
-
resolveTables,
|
|
808
|
-
getSyncMetaAll,
|
|
809
|
-
getStorageSyncMetaAll,
|
|
810
|
-
getStorageStatus,
|
|
811
|
-
getStoragePg,
|
|
812
|
-
getStorageMode,
|
|
813
|
-
getStorageDatabaseUrl,
|
|
814
|
-
getStorageDatabaseEnvName,
|
|
815
|
-
STORAGE_TABLES,
|
|
816
|
-
STORAGE_MODE_ENV,
|
|
817
|
-
STORAGE_DATABASE_ENV,
|
|
818
|
-
PgAdapterAsync,
|
|
819
|
-
PG_MIGRATIONS,
|
|
820
|
-
DOMAINS_STORAGE_TABLES,
|
|
821
|
-
DOMAINS_STORAGE_MODE_FALLBACK_ENV,
|
|
822
|
-
DOMAINS_STORAGE_MODE_ENV,
|
|
823
|
-
DOMAINS_STORAGE_FALLBACK_ENV,
|
|
824
|
-
DOMAINS_STORAGE_ENV
|
|
825
|
-
};
|