@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/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 = "cloud";
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 credentialDiskSources(name, env) {
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
- join(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
214
- join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
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 diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
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: "disk",
401
- source: winner.path,
476
+ tier: winner.src.tier,
477
+ source: winner.src.path,
402
478
  deliberate: false,
403
- deprecated: false,
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,CAW1B"}
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.45",
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": "mkdir -p $HOME/.hasna/domains 2>/dev/null || true",
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.13.4 artifact-scan domains-artifact-scan.tgz; rc=$?; rm -f \"$PWD/domains-artifact-scan.tgz\"; exit $rc"
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.0",
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
+ }