@hasna/domains 0.0.45 → 0.0.47
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 +1 -1
- package/dist/cli/index.js +760 -486
- package/dist/db/database.d.ts +5 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/index.js +563 -326
- package/dist/lib/app-home.d.ts +60 -0
- package/dist/lib/app-home.d.ts.map +1 -0
- package/dist/lib/config.d.ts +4 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/mcp/index.js +579 -342
- package/dist/sdk/index.js +93 -15
- package/dist/server/migrations.d.ts +35 -0
- package/dist/server/migrations.d.ts.map +1 -1
- package/package.json +7 -5
- package/postinstall.js +42 -0
package/dist/sdk/index.js
CHANGED
|
@@ -162,6 +162,7 @@ class DomainsClient {
|
|
|
162
162
|
}
|
|
163
163
|
// ../contracts/dist/client/transport.js
|
|
164
164
|
import { readFileSync, statSync } from "fs";
|
|
165
|
+
import { createRequire } from "module";
|
|
165
166
|
import { join } from "path";
|
|
166
167
|
function envToken(name) {
|
|
167
168
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -177,6 +178,9 @@ function credentialOverrideEnvKey(name) {
|
|
|
177
178
|
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
178
179
|
}
|
|
179
180
|
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
181
|
+
function credentialPointerEnvKey(name) {
|
|
182
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
183
|
+
}
|
|
180
184
|
|
|
181
185
|
class CredentialResolutionError extends Error {
|
|
182
186
|
appName;
|
|
@@ -189,31 +193,55 @@ class CredentialResolutionError extends Error {
|
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
var HASNA_STATE_DIR = ".hasna";
|
|
192
|
-
var FLEET_CREDENTIAL_DIR = "
|
|
196
|
+
var FLEET_CREDENTIAL_DIR = "fleet-env";
|
|
197
|
+
var LEGACY_CLOUD_DIR = "cloud";
|
|
193
198
|
var CONFIG_DIR = ".config";
|
|
194
199
|
var CONFIG_NAMESPACE = "hasna";
|
|
200
|
+
var LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01";
|
|
195
201
|
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
196
202
|
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
197
203
|
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
198
204
|
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
205
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
199
206
|
function homeDir(env) {
|
|
200
207
|
const home = env.HOME?.trim();
|
|
201
208
|
return home ? home : null;
|
|
202
209
|
}
|
|
203
|
-
function
|
|
204
|
-
return profileDiskSources(name, env, null);
|
|
205
|
-
}
|
|
206
|
-
function profileDiskSources(name, env, profile) {
|
|
210
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
207
211
|
const home = homeDir(env);
|
|
208
212
|
if (!home || !SAFE_APP_SLUG.test(name))
|
|
209
213
|
return [];
|
|
210
214
|
const stem = profile ? `${name}.${profile}` : name;
|
|
211
215
|
const configStem = profile ? `${name}-${profile}` : name;
|
|
212
216
|
return [
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
{
|
|
218
|
+
path: join(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
219
|
+
tier: "fleet-env",
|
|
220
|
+
deprecated: false
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
path: join(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
|
|
224
|
+
tier: "legacy-cloud",
|
|
225
|
+
deprecated: true
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
path: join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
|
|
229
|
+
tier: "config",
|
|
230
|
+
deprecated: false
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
path: join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
|
|
234
|
+
tier: "config-legacy",
|
|
235
|
+
deprecated: true
|
|
236
|
+
}
|
|
215
237
|
];
|
|
216
238
|
}
|
|
239
|
+
function credentialDiskSources(name, env) {
|
|
240
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
241
|
+
}
|
|
242
|
+
function profileDiskSources(name, env, profile) {
|
|
243
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
244
|
+
}
|
|
217
245
|
function parseEnvFile(text) {
|
|
218
246
|
const values = new Map;
|
|
219
247
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -264,6 +292,9 @@ function readCredentialFile(path, apiKeyKeys) {
|
|
|
264
292
|
return null;
|
|
265
293
|
}
|
|
266
294
|
function assertUsableCredential(appName, source, value) {
|
|
295
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
296
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
297
|
+
}
|
|
267
298
|
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
268
299
|
return;
|
|
269
300
|
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
@@ -287,6 +318,14 @@ function sealCredential(fields) {
|
|
|
287
318
|
writable: false,
|
|
288
319
|
configurable: false
|
|
289
320
|
});
|
|
321
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
322
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
323
|
+
value: fields.pointerVaultKey,
|
|
324
|
+
enumerable: false,
|
|
325
|
+
writable: false,
|
|
326
|
+
configurable: false
|
|
327
|
+
});
|
|
328
|
+
}
|
|
290
329
|
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
291
330
|
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
292
331
|
enumerable: false,
|
|
@@ -359,6 +398,27 @@ function resolveCredential(name, env, options = {}) {
|
|
|
359
398
|
warning: null
|
|
360
399
|
});
|
|
361
400
|
}
|
|
401
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
402
|
+
const pointerRaw = env[pointerKeyName];
|
|
403
|
+
if (pointerRaw !== undefined) {
|
|
404
|
+
const pointer = pointerRaw.trim();
|
|
405
|
+
if (!pointer) {
|
|
406
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
407
|
+
}
|
|
408
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
409
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
410
|
+
}
|
|
411
|
+
return sealCredential({
|
|
412
|
+
apiKey: "",
|
|
413
|
+
pointerVaultKey: pointer,
|
|
414
|
+
tier: "pointer",
|
|
415
|
+
source: pointerKeyName,
|
|
416
|
+
deliberate: true,
|
|
417
|
+
deprecated: false,
|
|
418
|
+
diskCandidates: diskPaths,
|
|
419
|
+
warning: null
|
|
420
|
+
});
|
|
421
|
+
}
|
|
362
422
|
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
363
423
|
if (profile) {
|
|
364
424
|
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
@@ -383,26 +443,42 @@ function resolveCredential(name, env, options = {}) {
|
|
|
383
443
|
}
|
|
384
444
|
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
385
445
|
}
|
|
386
|
-
const
|
|
446
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
447
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
387
448
|
if (diskHits.length > 0) {
|
|
388
449
|
const winner = diskHits[0];
|
|
389
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
450
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
390
451
|
const divergentSources = [
|
|
391
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
452
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
392
453
|
...(() => {
|
|
393
454
|
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
394
455
|
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
395
456
|
})()
|
|
396
457
|
];
|
|
397
|
-
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
458
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
459
|
+
let deprecated = winner.src.deprecated;
|
|
460
|
+
let finalWarning = warning;
|
|
461
|
+
if (winner.src.deprecated) {
|
|
462
|
+
deprecated = true;
|
|
463
|
+
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
464
|
+
const notified = deprecationNotified();
|
|
465
|
+
const noticeKey = `${name}:${winner.src.path}`;
|
|
466
|
+
if (!notified.has(noticeKey)) {
|
|
467
|
+
notified.add(noticeKey);
|
|
468
|
+
const target = diskSourceList[0]?.path ?? "<none>";
|
|
469
|
+
const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
|
|
470
|
+
sink(message);
|
|
471
|
+
}
|
|
472
|
+
finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
|
|
473
|
+
}
|
|
398
474
|
return sealCredential({
|
|
399
475
|
apiKey: winner.value,
|
|
400
|
-
tier:
|
|
401
|
-
source: winner.path,
|
|
476
|
+
tier: winner.src.tier,
|
|
477
|
+
source: winner.src.path,
|
|
402
478
|
deliberate: false,
|
|
403
|
-
deprecated
|
|
479
|
+
deprecated,
|
|
404
480
|
diskCandidates: diskPaths,
|
|
405
|
-
warning
|
|
481
|
+
warning: finalWarning
|
|
406
482
|
});
|
|
407
483
|
}
|
|
408
484
|
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
@@ -428,6 +504,8 @@ function resolveCredential(name, env, options = {}) {
|
|
|
428
504
|
}
|
|
429
505
|
return null;
|
|
430
506
|
}
|
|
507
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
508
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
431
509
|
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
432
510
|
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
433
511
|
"host",
|
|
@@ -12,6 +12,41 @@ import { type Migration, type MigrationResult } from "../generated/storage-kit/i
|
|
|
12
12
|
/** Env var holding the owner-role DSN (DDL privileges). Falls back to the app DSN. */
|
|
13
13
|
export declare const OWNER_DSN_ENV = "HASNA_DOMAINS_DATABASE_URL_OWNER";
|
|
14
14
|
export declare const APP_DSN_ENV = "HASNA_DOMAINS_DATABASE_URL";
|
|
15
|
+
/**
|
|
16
|
+
* Applied-ledger rows recorded before the ledger's id scheme stabilized, which
|
|
17
|
+
* no build in this repo's history generates (verified across every published
|
|
18
|
+
* tarball 0.0.30-0.0.46 and all git history). They are acknowledged via the
|
|
19
|
+
* storage kit's `acknowledgedLegacyIds`: they pass the downgrade guard, are
|
|
20
|
+
* never checksum-compared (their SQL is not reproducible from any source), and
|
|
21
|
+
* are never re-applied or re-inserted.
|
|
22
|
+
*
|
|
23
|
+
* `domains_apikeys_tenancy_0001`, `domains_apikeys_tenancy_0002`,
|
|
24
|
+
* `domains_tenancy_0001`..`domains_tenancy_0010` were recorded against the
|
|
25
|
+
* prod ledger out-of-band during the 2026-07 self-hosted cutover. Deploy
|
|
26
|
+
* evidence 2026-08-25: the 02:00Z pass failed on `_0001` (O15-00671 filed);
|
|
27
|
+
* the 16:44Z pass, carrying the kit with `_0001` acknowledged
|
|
28
|
+
* (hasna/apps#1176), advanced to `_0002`; the next pass then failed on the
|
|
29
|
+
* third row `domains_tenancy_0001` (O15-00758 filed), the 2026-08-25 PASS-18
|
|
30
|
+
* pass then failed on the fourth row `domains_tenancy_0002` at
|
|
31
|
+
* `domains-prod-migrate:42` (O15-00762 filed), and the 2026-08-25 pass then
|
|
32
|
+
* failed on the fifth row `domains_tenancy_0003` at `domains-prod-migrate:42`
|
|
33
|
+
* (O15-00766 filed). Deploy evidence 2026-08-26: passes 48-96 then failed on
|
|
34
|
+
* the sixth row `domains_tenancy_0004` at `domains-prod-migrate:44` through
|
|
35
|
+
* `:49` (todos 2b474505). Deploy evidence 2026-08-26/27: the 2026-08-27
|
|
36
|
+
* 06:47Z pass failed on the seventh row `domains_tenancy_0005` at
|
|
37
|
+
* `domains-prod-migrate:50` (O15-01822 filed). The ledger census (inspection
|
|
38
|
+
* task oss-fleet-prod/4b7c37626965439e96d5386c8e8a73d4) shows the complete
|
|
39
|
+
* out-of-band set is the 12 rows below — `domains_apikeys_tenancy_0001-0002`
|
|
40
|
+
* plus `domains_tenancy_0001..0010` — so this list covers the full measured
|
|
41
|
+
* prod ledger rather than one row per deploy pass. The apikeys rows'
|
|
42
|
+
* substance — the api-keys tenancy (`tid`) column — is carried today by
|
|
43
|
+
* `hasna_auth_0003_api_keys_tenant`; the substance of the `domains_tenancy_*`
|
|
44
|
+
* rows is not reproducible from any source in this repo, and no build
|
|
45
|
+
* generates the ids, so they are acknowledged as history rather than
|
|
46
|
+
* re-applied. Acknowledging all twelve unblocks `domains db migrate` (the ECS
|
|
47
|
+
* migrate task) and therefore the domains deploy lane.
|
|
48
|
+
*/
|
|
49
|
+
export declare const ACKNOWLEDGED_LEGACY_MIGRATION_IDS: readonly string[];
|
|
15
50
|
/** The ordered migration set: app schema first, then the shared api-keys table. */
|
|
16
51
|
export declare function buildMigrations(): Migration[];
|
|
17
52
|
/** Run all pending migrations against the owner DSN. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/server/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,mCAAmC,CAAC;AAK3C,sFAAsF;AACtF,eAAO,MAAM,aAAa,qCAAqC,CAAC;AAChE,eAAO,MAAM,WAAW,+BAA+B,CAAC;AAGxD,mFAAmF;AACnF,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAU7C;AAYD,wDAAwD;AACxD,wBAAsB,aAAa,CACjC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACvD,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/server/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,mCAAmC,CAAC;AAK3C,sFAAsF;AACtF,eAAO,MAAM,aAAa,qCAAqC,CAAC;AAChE,eAAO,MAAM,WAAW,+BAA+B,CAAC;AAGxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,iCAAiC,EAAE,SAAS,MAAM,EAa9D,CAAC;AAEF,mFAAmF;AACnF,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAU7C;AAYD,wDAAwD;AACxD,wBAAsB,aAAa,CACjC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACvD,OAAO,CAAC,eAAe,CAAC,CAa1B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/domains",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"description": "Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, local SQLite or cloud Postgres",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
49
|
"LICENSE",
|
|
50
|
-
"README.md"
|
|
50
|
+
"README.md",
|
|
51
|
+
"postinstall.js"
|
|
51
52
|
],
|
|
52
53
|
"scripts": {
|
|
53
54
|
"clean": "rm -rf dist",
|
|
@@ -60,9 +61,9 @@
|
|
|
60
61
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
61
62
|
"dev:serve": "bun run src/server/index.ts",
|
|
62
63
|
"prepublishOnly": "bun run build",
|
|
63
|
-
"postinstall": "
|
|
64
|
+
"postinstall": "node postinstall.js",
|
|
64
65
|
"prepack": "bun run build && bun run artifact-scan",
|
|
65
|
-
"artifact-scan": "bun pm pack --ignore-scripts --quiet --filename \"$PWD/domains-artifact-scan.tgz\" && bunx @hasna/contracts@0.
|
|
66
|
+
"artifact-scan": "bun pm pack --ignore-scripts --quiet --filename \"$PWD/domains-artifact-scan.tgz\" && bunx @hasna/contracts@0.14.1 artifact-scan domains-artifact-scan.tgz; rc=$?; rm -f \"$PWD/domains-artifact-scan.tgz\"; exit $rc"
|
|
66
67
|
},
|
|
67
68
|
"keywords": [
|
|
68
69
|
"domains",
|
|
@@ -97,7 +98,8 @@
|
|
|
97
98
|
"@aws-sdk/client-route-53": "^3.1067.0",
|
|
98
99
|
"@aws-sdk/client-route-53-domains": "^3.1067.0",
|
|
99
100
|
"@aws-sdk/credential-provider-ini": "3.972.53",
|
|
100
|
-
"@hasna/contracts": "0.14.
|
|
101
|
+
"@hasna/contracts": "0.14.1",
|
|
102
|
+
"@hasna/paths": "0.1.0",
|
|
101
103
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
102
104
|
"chalk": "^5.4.1",
|
|
103
105
|
"commander": "^13.1.0",
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Best-effort install-time creation of the domains home directory, resolving
|
|
2
|
+
// the SAME effective home the runtime uses (src/lib/app-home.ts): an exact-app
|
|
3
|
+
// override (HASNA_DOMAINS_HOME / DOMAINS_HOME / HASNA_DOMAINS_DIR / DOMAINS_DIR)
|
|
4
|
+
// wins; otherwise the @hasna/paths XDG data home once adopted (HASNA_DATA_HOME
|
|
5
|
+
// set, or domains.db already migrated there); otherwise the legacy
|
|
6
|
+
// ~/.hasna/domains default. Failures are non-fatal: the runtime creates the
|
|
7
|
+
// same directory on first use.
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const { dataDir } = await import("@hasna/paths");
|
|
14
|
+
const env = process.env;
|
|
15
|
+
const override = (
|
|
16
|
+
env["HASNA_DOMAINS_HOME"] ||
|
|
17
|
+
env["DOMAINS_HOME"] ||
|
|
18
|
+
env["HASNA_DOMAINS_DIR"] ||
|
|
19
|
+
env["DOMAINS_DIR"] ||
|
|
20
|
+
""
|
|
21
|
+
).trim();
|
|
22
|
+
const dataHomeOverride = (env["HASNA_DATA_HOME"] || "").trim();
|
|
23
|
+
const home = env["HOME"] || env["USERPROFILE"] || homedir();
|
|
24
|
+
|
|
25
|
+
let dir;
|
|
26
|
+
if (override) {
|
|
27
|
+
dir = override;
|
|
28
|
+
} else {
|
|
29
|
+
const resolved = dataDir({ app: "domains", home, env });
|
|
30
|
+
const adopted = Boolean(dataHomeOverride) || existsSync(join(resolved, "domains.db"));
|
|
31
|
+
dir = adopted ? resolved : join(home, ".hasna", "domains");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
35
|
+
try {
|
|
36
|
+
chmodSync(dir, 0o700);
|
|
37
|
+
} catch {
|
|
38
|
+
// best-effort on platforms without POSIX perms
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// never fail an install over pre-created directories
|
|
42
|
+
}
|