@hasna/domains 0.0.39 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +11 -15
  2. package/dist/cli/commands/domain.d.ts.map +1 -1
  3. package/dist/cli/index.js +19736 -32040
  4. package/dist/db/database.d.ts +24 -0
  5. package/dist/db/database.d.ts.map +1 -1
  6. package/dist/db/domain-records.d.ts +61 -2
  7. package/dist/db/domain-records.d.ts.map +1 -1
  8. package/dist/db/domains.d.ts +14 -7
  9. package/dist/db/domains.d.ts.map +1 -1
  10. package/dist/db/migrations.d.ts.map +1 -1
  11. package/dist/db/pg-migrations.d.ts.map +1 -1
  12. package/dist/db/store.d.ts +48 -48
  13. package/dist/db/store.d.ts.map +1 -1
  14. package/dist/generated/storage-kit/backend.d.ts +19 -0
  15. package/dist/generated/storage-kit/backend.d.ts.map +1 -0
  16. package/dist/generated/storage-kit/index.d.ts +2 -2
  17. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  18. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  19. package/dist/generated/storage-kit/own.d.ts +11 -0
  20. package/dist/generated/storage-kit/own.d.ts.map +1 -0
  21. package/dist/generated/storage-kit/pool.d.ts +7 -6
  22. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  23. package/dist/generated/storage-kit/query.d.ts +1 -1
  24. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  25. package/dist/generated/storage-kit/tls.d.ts +30 -3
  26. package/dist/generated/storage-kit/tls.d.ts.map +1 -1
  27. package/dist/index.js +15865 -28359
  28. package/dist/lib/brandsight.d.ts.map +1 -1
  29. package/dist/lib/config.d.ts +23 -3
  30. package/dist/lib/config.d.ts.map +1 -1
  31. package/dist/lib/freshness.d.ts +65 -0
  32. package/dist/lib/freshness.d.ts.map +1 -0
  33. package/dist/lib/godaddy.d.ts.map +1 -1
  34. package/dist/lib/namecheap.d.ts +2 -0
  35. package/dist/lib/namecheap.d.ts.map +1 -1
  36. package/dist/lib/registrar.js +17 -6
  37. package/dist/lib/route53.d.ts.map +1 -1
  38. package/dist/lib/route53.js +5 -2
  39. package/dist/mcp/index.js +13479 -25921
  40. package/dist/sdk/index.d.ts +3 -2
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +280 -3
  43. package/dist/server/app.d.ts +12 -8
  44. package/dist/server/app.d.ts.map +1 -1
  45. package/dist/server/index.d.ts +4 -5
  46. package/dist/server/index.d.ts.map +1 -1
  47. package/dist/server/index.js +527 -181
  48. package/dist/server/openapi.d.ts.map +1 -1
  49. package/dist/server/repo.d.ts.map +1 -1
  50. package/package.json +7 -4
  51. package/dist/generated/storage-kit/mode.d.ts +0 -48
  52. package/dist/generated/storage-kit/mode.d.ts.map +0 -1
@@ -1,14 +1,90 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
 
4
- // node_modules/@hasna/contracts/dist/auth/index.js
4
+ // ../contracts/dist/auth/index.js
5
5
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
6
+ var MAX_TENANT_ID_LENGTH = 64;
7
+ var TENANT_ID_PATTERN = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]{0,${MAX_TENANT_ID_LENGTH - 1}}$`);
8
+ var UUID_HEX = "[0-9a-fA-F]";
9
+ var UUID_PATTERN = new RegExp(`^\\{?(?:${UUID_HEX}{8}-${UUID_HEX}{4}-${UUID_HEX}{4}-${UUID_HEX}{4}-${UUID_HEX}{12}|${UUID_HEX}{32})\\}?$`);
10
+ function isValidTenantId(value) {
11
+ return typeof value === "string" && TENANT_ID_PATTERN.test(value);
12
+ }
13
+ function isUuidTenantId(value) {
14
+ return typeof value === "string" && UUID_PATTERN.test(value);
15
+ }
16
+ function canonicalizeTenantId(value) {
17
+ if (!isUuidTenantId(value))
18
+ return value;
19
+ const hex = value.replace(/[{}-]/g, "").toLowerCase();
20
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21
+ }
22
+ function normalizeTenantId(value) {
23
+ const trimmed = typeof value === "string" ? value.trim() : "";
24
+ const canonical = canonicalizeTenantId(trimmed);
25
+ if (!isValidTenantId(canonical)) {
26
+ throw new Error(`Invalid tenant id '${value}'. Expected 1-${MAX_TENANT_ID_LENGTH} characters matching ${TENANT_ID_PATTERN} (a UUID, ULID, slug, or prefixed id).`);
27
+ }
28
+ return canonical;
29
+ }
30
+ function tenantIdsEqual(left, right) {
31
+ const canonical = (value) => {
32
+ if (typeof value !== "string")
33
+ return null;
34
+ const folded = canonicalizeTenantId(value.trim());
35
+ return isValidTenantId(folded) ? folded : null;
36
+ };
37
+ const a = canonical(left);
38
+ const b = canonical(right);
39
+ return a !== null && b !== null && a === b;
40
+ }
41
+ function ownTenantId(source) {
42
+ return Object.hasOwn(source, "tid") ? source.tid : undefined;
43
+ }
6
44
  var API_KEY_TOKEN_VERSION = 1;
7
45
  var API_KEY_NAMESPACE = "hasna";
8
- var TOKEN_PATTERN = /^hasna_([a-z][a-z0-9-]*)_([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/;
46
+ var API_KEY_TOKEN_PATTERN = /^hasna_([a-z][a-z0-9-]*)_([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/;
47
+ var TOKEN_PATTERN = API_KEY_TOKEN_PATTERN;
9
48
  var DEFAULT_API_KEY_TTL_SECONDS = 90 * 24 * 60 * 60;
49
+ function ownAgentClaim(source) {
50
+ return Object.hasOwn(source, "agent") && typeof source.agent === "string" ? source.agent : null;
51
+ }
52
+ function ownScopesClaim(source) {
53
+ return Object.hasOwn(source, "scopes") && Array.isArray(source.scopes) ? source.scopes : null;
54
+ }
55
+ function ownOption(options, name) {
56
+ return Object.hasOwn(options, name) ? options[name] : undefined;
57
+ }
58
+ var typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
59
+ var intrinsicViewBuffer = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
60
+ var intrinsicViewByteOffset = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset").get;
61
+ var intrinsicViewByteLength = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
62
+ var intrinsicDataViewBuffer = Object.getOwnPropertyDescriptor(DataView.prototype, "buffer").get;
63
+ var intrinsicDataViewByteOffset = Object.getOwnPropertyDescriptor(DataView.prototype, "byteOffset").get;
64
+ var intrinsicDataViewByteLength = Object.getOwnPropertyDescriptor(DataView.prototype, "byteLength").get;
65
+ function viewWindow(view) {
66
+ try {
67
+ return [
68
+ intrinsicViewBuffer.call(view),
69
+ intrinsicViewByteOffset.call(view),
70
+ intrinsicViewByteLength.call(view)
71
+ ];
72
+ } catch {
73
+ return [
74
+ intrinsicDataViewBuffer.call(view),
75
+ intrinsicDataViewByteOffset.call(view),
76
+ intrinsicDataViewByteLength.call(view)
77
+ ];
78
+ }
79
+ }
10
80
  function toBuffer(secret) {
11
- return typeof secret === "string" ? Buffer.from(secret, "utf8") : secret;
81
+ if (typeof secret === "string")
82
+ return Buffer.from(secret, "utf8");
83
+ if (ArrayBuffer.isView(secret)) {
84
+ const [store, byteOffset, byteLength] = viewWindow(secret);
85
+ return Buffer.from(store, byteOffset, byteLength);
86
+ }
87
+ return Buffer.from(secret);
12
88
  }
13
89
  function hmac(signingSecret, message) {
14
90
  return createHmac("sha256", toBuffer(signingSecret)).update(message, "utf8").digest();
@@ -31,12 +107,23 @@ function parseApiKey(token) {
31
107
  } catch {
32
108
  return null;
33
109
  }
34
- if (typeof claims !== "object" || claims === null || typeof claims.kid !== "string" || typeof claims.app !== "string" || !Array.isArray(claims.scopes)) {
110
+ if (typeof claims !== "object" || claims === null || typeof claims.kid !== "string" || typeof claims.app !== "string" || ownScopesClaim(claims) === null) {
111
+ return null;
112
+ }
113
+ const claimedTid = ownTenantId(claims);
114
+ if (claimedTid !== undefined && !isValidTenantId(claimedTid)) {
35
115
  return null;
36
116
  }
37
117
  return { app, body, sig, claims };
38
118
  }
39
119
  function verifyApiKeyToken(token, options) {
120
+ const optSigningSecret = ownOption(options, "signingSecret");
121
+ const optExpectedApp = ownOption(options, "expectedApp");
122
+ const optNowMs = ownOption(options, "nowMs");
123
+ const optLeewaySeconds = ownOption(options, "leewaySeconds");
124
+ const optRequiredScopes = ownOption(options, "requiredScopes");
125
+ const optRequireTenant = ownOption(options, "requireTenant");
126
+ const optExpectedTid = ownOption(options, "expectedTid");
40
127
  const parsed = parseApiKey(token);
41
128
  if (!parsed) {
42
129
  return { ok: false, reason: "malformed", message: "Token is malformed." };
@@ -48,10 +135,10 @@ function verifyApiKeyToken(token, options) {
48
135
  if (claims.app !== app) {
49
136
  return { ok: false, reason: "app_mismatch", message: "Token prefix app does not match claims." };
50
137
  }
51
- if (options.expectedApp !== undefined && app !== options.expectedApp) {
52
- return { ok: false, reason: "app_mismatch", message: `Token is for app '${app}', expected '${options.expectedApp}'.` };
138
+ if (optExpectedApp !== undefined && app !== optExpectedApp) {
139
+ return { ok: false, reason: "app_mismatch", message: `Token is for app '${app}', expected '${optExpectedApp}'.` };
53
140
  }
54
- const expected = hmac(options.signingSecret, `${apiKeyPrefix(app)}${body}`);
141
+ const expected = hmac(optSigningSecret, `${apiKeyPrefix(app)}${body}`);
55
142
  let provided;
56
143
  try {
57
144
  provided = Buffer.from(sig, "base64url");
@@ -61,16 +148,41 @@ function verifyApiKeyToken(token, options) {
61
148
  if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
62
149
  return { ok: false, reason: "bad_signature", message: "Signature verification failed." };
63
150
  }
64
- const now = Math.floor((options.nowMs ?? Date.now()) / 1000);
65
- const leeway = options.leewaySeconds ?? 0;
151
+ const agent = ownAgentClaim(claims);
152
+ const now = Math.floor((optNowMs ?? Date.now()) / 1000);
153
+ const leeway = optLeewaySeconds ?? 0;
66
154
  if (typeof claims.iat === "number" && now + leeway < claims.iat) {
67
- return { ok: false, reason: "not_yet_valid", message: "Token is not yet valid." };
155
+ return { ok: false, reason: "not_yet_valid", message: "Token is not yet valid.", agent };
68
156
  }
69
157
  if (claims.exp !== null && typeof claims.exp === "number" && now - leeway >= claims.exp) {
70
- return { ok: false, reason: "expired", message: "Token has expired." };
158
+ return { ok: false, reason: "expired", message: "Token has expired.", agent };
159
+ }
160
+ const verifiedTid = ownTenantId(claims);
161
+ const tid = verifiedTid === undefined ? null : canonicalizeTenantId(verifiedTid);
162
+ const tenantRequired = Boolean(optRequireTenant) || optExpectedTid !== undefined;
163
+ if (tenantRequired && tid === null) {
164
+ return {
165
+ ok: false,
166
+ reason: "tenant_required",
167
+ message: "Token carries no tenant id ('tid') and this service requires one.",
168
+ kid: claims.kid,
169
+ tid: null,
170
+ agent
171
+ };
172
+ }
173
+ if (optExpectedTid !== undefined && !tenantIdsEqual(tid, optExpectedTid)) {
174
+ const expectationIsWellFormed = typeof optExpectedTid === "string" && isValidTenantId(optExpectedTid.trim());
175
+ return {
176
+ ok: false,
177
+ reason: "tenant_mismatch",
178
+ message: expectationIsWellFormed ? "Token is for a different tenant than the one this service accepts." : "Token tenant cannot be checked: the expected tenant id is not a valid tenant id.",
179
+ kid: claims.kid,
180
+ tid,
181
+ agent
182
+ };
71
183
  }
72
- if (options.requiredScopes && options.requiredScopes.length > 0) {
73
- const granted = claims.scopes;
184
+ if (optRequiredScopes && optRequiredScopes.length > 0) {
185
+ const granted = ownScopesClaim(claims) ?? [];
74
186
  const satisfies = (required) => granted.some((g) => {
75
187
  if (g === "*")
76
188
  return true;
@@ -84,15 +196,16 @@ function verifyApiKeyToken(token, options) {
84
196
  const rAction = required.slice(ri + 1);
85
197
  return (gApp === "*" || gApp === rApp) && (gAction === "*" || gAction === rAction);
86
198
  });
87
- for (const required of options.requiredScopes) {
199
+ for (const required of optRequiredScopes) {
88
200
  if (!satisfies(required)) {
89
- return { ok: false, reason: "insufficient_scope", message: `Missing required scope '${required}'.` };
201
+ return { ok: false, reason: "insufficient_scope", message: `Missing required scope '${required}'.`, agent };
90
202
  }
91
203
  }
92
204
  }
93
- return { ok: true, claims, kid: claims.kid, app };
205
+ return { ok: true, claims, kid: claims.kid, app, tid, agent };
94
206
  }
95
207
  var DEFAULT_API_KEYS_TABLE = "api_keys";
208
+ var API_KEY_ISSUANCE_PENDING_REASON = "credential_delivery_pending";
96
209
  function createTableSql(table) {
97
210
  return `CREATE TABLE IF NOT EXISTS ${table} (
98
211
  kid TEXT PRIMARY KEY,
@@ -116,6 +229,11 @@ function apiKeyMigrations(table = DEFAULT_API_KEYS_TABLE) {
116
229
  id: `hasna_auth_0002_${table}_indexes`,
117
230
  sql: `CREATE INDEX IF NOT EXISTS ${table}_app_idx ON ${table} (app);
118
231
  CREATE INDEX IF NOT EXISTS ${table}_token_hash_idx ON ${table} (token_hash);`
232
+ },
233
+ {
234
+ id: `hasna_auth_0003_${table}_tenant`,
235
+ sql: `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS tid TEXT;
236
+ CREATE INDEX IF NOT EXISTS ${table}_tid_idx ON ${table} (tid);`
119
237
  }
120
238
  ];
121
239
  }
@@ -140,10 +258,13 @@ function parseScopes(value) {
140
258
  return [];
141
259
  }
142
260
  function rowToRecord(row) {
261
+ const tid = ownTenantId(row);
262
+ const agentValue = Object.hasOwn(row, "agent") ? row.agent : null;
143
263
  return {
144
264
  kid: String(row.kid),
145
265
  app: String(row.app),
146
- agent: row.agent === null || row.agent === undefined ? null : String(row.agent),
266
+ agent: agentValue === null || agentValue === undefined ? null : String(agentValue),
267
+ tid: tid === null || tid === undefined ? null : String(tid),
147
268
  scopes: parseScopes(row.scopes),
148
269
  tokenHash: String(row.token_hash),
149
270
  issuedAt: toIso(row.issued_at) ?? new Date(0).toISOString(),
@@ -174,31 +295,63 @@ class ApiKeyStore {
174
295
  }
175
296
  }
176
297
  async insert(input) {
298
+ await this.insertWithLifecycle(input, null, null);
299
+ }
300
+ async insertWithLifecycle(input, revokedAt, revokedReason) {
301
+ const tid = ownTenantId(input);
302
+ const agent = ownAgentClaim(input);
177
303
  await this.client.execute(`INSERT INTO ${this.table}
178
- (kid, app, agent, scopes, token_hash, issued_at, expires_at, created_by)
179
- VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8)`, [
304
+ (kid, app, agent, tid, scopes, token_hash, issued_at, expires_at, created_by, revoked_at, revoked_reason)
305
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11)`, [
180
306
  input.kid,
181
307
  input.app,
182
- input.agent ?? null,
308
+ agent,
309
+ tid === undefined || tid === null ? null : normalizeTenantId(tid),
183
310
  JSON.stringify(input.scopes),
184
311
  input.tokenHash,
185
312
  input.issuedAt.toISOString(),
186
313
  input.expiresAt ? input.expiresAt.toISOString() : null,
187
- input.createdBy ?? null
314
+ input.createdBy ?? null,
315
+ revokedAt,
316
+ revokedReason
188
317
  ]);
189
318
  }
190
- async insertMinted(minted, createdBy) {
319
+ mintedInput(minted, createdBy) {
191
320
  const claims = minted.claims;
192
- await this.insert({
321
+ return {
193
322
  kid: minted.kid,
194
323
  app: claims.app,
195
- agent: claims.agent ?? null,
324
+ agent: ownAgentClaim(claims),
325
+ tid: ownTenantId(claims) ?? null,
196
326
  scopes: claims.scopes,
197
327
  tokenHash: minted.tokenHash,
198
328
  issuedAt: new Date(claims.iat * 1000),
199
329
  expiresAt: claims.exp === null ? null : new Date(claims.exp * 1000),
200
330
  createdBy: createdBy ?? null
201
- });
331
+ };
332
+ }
333
+ async insertMinted(minted, createdBy) {
334
+ await this.insert(this.mintedInput(minted, createdBy));
335
+ }
336
+ async insertMintedPending(minted, createdBy, atMs = Date.now()) {
337
+ await this.insertWithLifecycle(this.mintedInput(minted, createdBy), new Date(atMs).toISOString(), API_KEY_ISSUANCE_PENDING_REASON);
338
+ }
339
+ async activatePending(kid, tokenHash) {
340
+ const row = await this.client.get(`UPDATE ${this.table}
341
+ SET revoked_at = NULL, revoked_reason = NULL
342
+ WHERE kid = $1
343
+ AND revoked_at IS NOT NULL
344
+ AND revoked_reason = $2
345
+ AND token_hash = $3
346
+ RETURNING kid`, [kid, API_KEY_ISSUANCE_PENDING_REASON, tokenHash]);
347
+ if (row)
348
+ return true;
349
+ const active = await this.client.get(`SELECT kid FROM ${this.table}
350
+ WHERE kid = $1
351
+ AND token_hash = $2
352
+ AND revoked_at IS NULL
353
+ AND revoked_reason IS NULL`, [kid, tokenHash]);
354
+ return active !== null;
202
355
  }
203
356
  async findByKid(kid) {
204
357
  const row = await this.client.get(`SELECT * FROM ${this.table} WHERE kid = $1`, [kid]);
@@ -224,6 +377,9 @@ class ApiKeyStore {
224
377
  return "expired";
225
378
  return "active";
226
379
  }
380
+ keyStatus = async (kid) => {
381
+ return this.status(kid);
382
+ };
227
383
  statusChecker() {
228
384
  return async (kid) => {
229
385
  const status = await this.status(kid);
@@ -250,11 +406,16 @@ class ApiKeyStore {
250
406
  params.push(options.app);
251
407
  clauses.push(`app = $${params.length}`);
252
408
  }
409
+ const tid = ownTenantId(options);
410
+ if (tid !== undefined) {
411
+ params.push(normalizeTenantId(tid));
412
+ clauses.push(`tid = $${params.length}`);
413
+ }
253
414
  if (!options.includeRevoked) {
254
415
  clauses.push("revoked_at IS NULL");
255
416
  }
256
417
  const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
257
- const rows = await this.client.many(`SELECT * FROM ${this.table} ${where} ORDER BY issued_at DESC`);
418
+ const rows = await this.client.many(`SELECT * FROM ${this.table} ${where} ORDER BY issued_at DESC`, params);
258
419
  return rows.map(rowToRecord);
259
420
  }
260
421
  async revokedKids() {
@@ -291,27 +452,65 @@ function extractToken(source, headerName = "x-api-key", scheme = "Bearer") {
291
452
  }
292
453
  return null;
293
454
  }
455
+ function ownOption2(bag, name) {
456
+ return Object.hasOwn(bag, name) ? bag[name] : undefined;
457
+ }
294
458
  function verifyApiKey(options) {
295
- if (!options.app)
459
+ const optionApp = ownOption2(options, "app");
460
+ const optionSigningSecret = ownOption2(options, "signingSecret");
461
+ const optionExpectedTid = ownOption2(options, "expectedTid");
462
+ const optionRequiredScopes = ownOption2(options, "requiredScopes");
463
+ const optionRequireTenant = ownOption2(options, "requireTenant");
464
+ const optionLeewaySeconds = ownOption2(options, "leewaySeconds");
465
+ const audit = ownOption2(options, "audit");
466
+ const headerName = ownOption2(options, "headerName") ?? "x-api-key";
467
+ const scheme = ownOption2(options, "scheme") ?? "Bearer";
468
+ const clock = ownOption2(options, "nowMs") ?? (() => Date.now());
469
+ if (!optionApp)
296
470
  throw new Error("verifyApiKey requires an 'app' slug.");
297
- if (!options.signingSecret) {
471
+ if (!optionSigningSecret) {
298
472
  throw new Error("verifyApiKey requires a 'signingSecret'. Set it from HASNA_<APP>_API_SIGNING_KEY.");
299
473
  }
300
- const headerName = options.headerName ?? "x-api-key";
301
- const scheme = options.scheme ?? "Bearer";
302
- const clock = options.nowMs ?? (() => Date.now());
474
+ if (optionExpectedTid !== undefined && !isValidTenantId(optionExpectedTid)) {
475
+ throw new Error(`verifyApiKey received an invalid 'expectedTid': '${optionExpectedTid}'.`);
476
+ }
477
+ const app = optionApp;
478
+ const signingSecret = optionSigningSecret;
479
+ const ownKeyStatus = ownOption2(options, "keyStatus");
480
+ const ownIsRevoked = ownOption2(options, "isRevoked");
481
+ const allowUnregistered = ownOption2(options, "allowUnregisteredKeys") === true;
482
+ if (ownKeyStatus && ownIsRevoked) {
483
+ throw new Error("verifyApiKey received both 'keyStatus' and 'isRevoked'. Supply exactly one \u2014 " + "letting one silently win would hide which check is actually guarding the service. " + "Use 'keyStatus' (store.keyStatus); drop 'isRevoked'.");
484
+ }
485
+ if (!ownKeyStatus && !allowUnregistered) {
486
+ throw new Error(ownIsRevoked ? "verifyApiKey was given only 'isRevoked', which cannot refuse a key this service has " + "no record of: it returns false both for an active key and for one that was never " + "registered, so an unregistered key is irrevocable. Wire 'keyStatus: store.keyStatus' " + "(or 'isRevoked: store.statusChecker()'), or set 'allowUnregisteredKeys: true' to " + "accept that risk explicitly." : "verifyApiKey requires a key-status hook. Without one this service performs NO " + "revocation check and cannot turn any of its keys off. Wire " + "'keyStatus: store.keyStatus', or set 'allowUnregisteredKeys: true' to declare that " + "this service intentionally cannot revoke keys.");
487
+ }
303
488
  async function emit(event) {
304
- if (!options.audit)
489
+ if (!audit)
305
490
  return;
306
491
  try {
307
- await options.audit(event);
492
+ await audit(event);
308
493
  } catch {}
309
494
  }
310
495
  async function authenticate(headers, context = {}) {
311
- const method = context.method ?? null;
312
- const path = context.path ?? null;
313
- const requiredScopes = [...options.requiredScopes ?? [], ...context.requiredScopes ?? []];
496
+ const method = ownOption2(context, "method") ?? null;
497
+ const path = ownOption2(context, "path") ?? null;
498
+ const requiredScopes = [
499
+ ...optionRequiredScopes ?? [],
500
+ ...ownOption2(context, "requiredScopes") ?? []
501
+ ];
314
502
  const at = new Date(clock()).toISOString();
503
+ const perCallTid = ownOption2(context, "expectedTid");
504
+ const expectedTid = perCallTid !== undefined ? perCallTid : optionExpectedTid;
505
+ if (perCallTid !== undefined && optionExpectedTid !== undefined && !tenantIdsEqual(perCallTid, optionExpectedTid)) {
506
+ await emit({ outcome: "deny", app, kid: null, tid: null, reason: "tenant_mismatch", scopesRequired: requiredScopes, method, path, status: 403, at });
507
+ return {
508
+ ok: false,
509
+ status: 403,
510
+ reason: "tenant_mismatch",
511
+ message: "This route addresses a tenant other than the one this service is pinned to."
512
+ };
513
+ }
315
514
  const token = extractToken(headers, headerName, scheme);
316
515
  if (!token) {
317
516
  const decision = {
@@ -320,25 +519,72 @@ function verifyApiKey(options) {
320
519
  reason: "missing_token",
321
520
  message: `Missing API key. Send it as '${headerName}: <key>' or 'Authorization: ${scheme} <key>'.`
322
521
  };
323
- await emit({ outcome: "deny", app: options.app, kid: null, reason: "missing_token", scopesRequired: requiredScopes, method, path, status: 401, at });
522
+ await emit({ outcome: "deny", app, kid: null, tid: null, reason: "missing_token", scopesRequired: requiredScopes, method, path, status: 401, at });
324
523
  return decision;
325
524
  }
326
525
  const verified = verifyApiKeyToken(token, {
327
- signingSecret: options.signingSecret,
328
- expectedApp: options.app,
526
+ signingSecret,
527
+ expectedApp: app,
329
528
  nowMs: clock(),
330
- ...options.leewaySeconds !== undefined ? { leewaySeconds: options.leewaySeconds } : {},
529
+ ...optionLeewaySeconds !== undefined ? { leewaySeconds: optionLeewaySeconds } : {},
530
+ ...optionRequireTenant !== undefined ? { requireTenant: optionRequireTenant } : {},
531
+ ...expectedTid !== undefined ? { expectedTid } : {},
331
532
  requiredScopes
332
533
  });
333
534
  if (!verified.ok) {
334
- const status = verified.reason === "insufficient_scope" ? 403 : 401;
335
- await emit({ outcome: "deny", app: options.app, kid: null, reason: verified.reason, scopesRequired: requiredScopes, method, path, status, at });
535
+ const status = verified.reason === "insufficient_scope" || verified.reason === "tenant_mismatch" || verified.reason === "tenant_required" ? 403 : 401;
536
+ await emit({
537
+ outcome: "deny",
538
+ app,
539
+ kid: ownOption2(verified, "kid") ?? null,
540
+ tid: ownTenantId(verified) ?? null,
541
+ ...Object.hasOwn(verified, "agent") ? { agent: verified.agent } : {},
542
+ reason: verified.reason,
543
+ scopesRequired: requiredScopes,
544
+ method,
545
+ path,
546
+ status,
547
+ at
548
+ });
336
549
  return { ok: false, status, reason: verified.reason, message: verified.message };
337
550
  }
338
- if (options.isRevoked) {
339
- const revoked = await options.isRevoked(verified.kid);
551
+ if (ownKeyStatus) {
552
+ let status;
553
+ try {
554
+ status = await ownKeyStatus(verified.kid);
555
+ } catch {
556
+ await emit({ outcome: "deny", app, kid: verified.kid, tid: verified.tid, agent: verified.agent, reason: "status_unavailable", scopesRequired: requiredScopes, method, path, status: 503, at });
557
+ return {
558
+ ok: false,
559
+ status: 503,
560
+ reason: "status_unavailable",
561
+ message: "Could not verify API key status. Try again shortly."
562
+ };
563
+ }
564
+ if (status !== "active") {
565
+ const known = status === "revoked" || status === "expired" || status === "unknown";
566
+ if (!(status === "unknown" && allowUnregistered)) {
567
+ const reason = status === "revoked" || status === "expired" ? status : "unknown_key";
568
+ const message = reason === "unknown_key" ? known ? "API key is not registered with this service." : "API key status could not be recognized." : status === "expired" ? "API key has expired." : "API key has been revoked.";
569
+ await emit({ outcome: "deny", app, kid: verified.kid, tid: verified.tid, agent: verified.agent, reason, scopesRequired: requiredScopes, method, path, status: 401, at });
570
+ return { ok: false, status: 401, reason, message };
571
+ }
572
+ }
573
+ } else if (ownIsRevoked) {
574
+ let revoked;
575
+ try {
576
+ revoked = await ownIsRevoked(verified.kid);
577
+ } catch {
578
+ await emit({ outcome: "deny", app, kid: verified.kid, tid: verified.tid, agent: verified.agent, reason: "status_unavailable", scopesRequired: requiredScopes, method, path, status: 503, at });
579
+ return {
580
+ ok: false,
581
+ status: 503,
582
+ reason: "status_unavailable",
583
+ message: "Could not verify API key status. Try again shortly."
584
+ };
585
+ }
340
586
  if (revoked) {
341
- await emit({ outcome: "deny", app: options.app, kid: verified.kid, reason: "revoked", scopesRequired: requiredScopes, method, path, status: 401, at });
587
+ await emit({ outcome: "deny", app, kid: verified.kid, tid: verified.tid, agent: verified.agent, reason: "revoked", scopesRequired: requiredScopes, method, path, status: 401, at });
342
588
  return { ok: false, status: 401, reason: "revoked", message: "API key has been revoked." };
343
589
  }
344
590
  }
@@ -346,138 +592,202 @@ function verifyApiKey(options) {
346
592
  kid: verified.kid,
347
593
  app: verified.app,
348
594
  scopes: verified.claims.scopes,
349
- agent: verified.claims.agent ?? null,
595
+ agent: verified.agent,
596
+ tid: verified.tid,
350
597
  claims: verified.claims
351
598
  };
352
- await emit({ outcome: "allow", app: options.app, kid: verified.kid, reason: null, scopesRequired: requiredScopes, method, path, status: 200, at });
599
+ await emit({ outcome: "allow", app, kid: verified.kid, tid: verified.tid, agent: verified.agent, reason: null, scopesRequired: requiredScopes, method, path, status: 200, at });
353
600
  return { ok: true, status: 200, principal };
354
601
  }
355
- return { authenticate, app: options.app };
602
+ return { authenticate, app };
356
603
  }
604
+ var MAX_FLEET_TOKEN_TTL_SECONDS = 24 * 60 * 60;
357
605
 
358
- // src/generated/storage-kit/mode.ts
359
- var DEPRECATED_STORAGE_MODE_ALIASES = [
360
- "remote",
361
- "hybrid",
362
- "self_hosted"
363
- ];
364
- function normalizeStorageMode(value) {
365
- const normalized = value.trim().toLowerCase().replace(/-/g, "_");
366
- if (normalized === "local")
367
- return { mode: "local", deprecatedAlias: null };
368
- if (normalized === "cloud")
369
- return { mode: "cloud", deprecatedAlias: null };
370
- if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
371
- return { mode: "cloud", deprecatedAlias: normalized };
372
- }
373
- throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
606
+ // src/generated/storage-kit/own.ts
607
+ function ownProp(source, key) {
608
+ if (source === null || source === undefined)
609
+ return;
610
+ const kind = typeof source;
611
+ if (kind !== "object" && kind !== "function")
612
+ return;
613
+ if (!Object.hasOwn(source, key))
614
+ return;
615
+ return source[key];
616
+ }
617
+ function ownString(source, key) {
618
+ const value = ownProp(source, key);
619
+ return typeof value === "string" ? value : undefined;
374
620
  }
621
+
622
+ // src/generated/storage-kit/backend.ts
375
623
  function envToken(name) {
376
624
  return name.toUpperCase().replace(/-/g, "_");
377
625
  }
378
- function storageEnvKeys(name) {
626
+ function serverDataBackendEnvKeys(name) {
379
627
  const token = envToken(name);
380
628
  return {
381
- modeKeys: [`HASNA_${token}_STORAGE_MODE`, `${token}_STORAGE_MODE`],
382
629
  databaseUrlKeys: [`HASNA_${token}_DATABASE_URL`, `${token}_DATABASE_URL`]
383
630
  };
384
631
  }
385
632
  function firstEnv(env, keys) {
386
633
  for (const key of keys) {
387
- const value = env[key]?.trim();
634
+ const value = ownString(env, key)?.trim();
388
635
  if (value)
389
636
  return { key, value };
390
637
  }
391
638
  return null;
392
639
  }
393
- function resolveStorageMode(name, env = process.env) {
394
- const { modeKeys, databaseUrlKeys } = storageEnvKeys(name);
395
- const dbHit = firstEnv(env, databaseUrlKeys);
396
- const databaseUrlPresent = Boolean(dbHit);
397
- const databaseUrlSource = dbHit ? dbHit.key : null;
398
- const modeHit = firstEnv(env, modeKeys);
399
- if (!modeHit) {
640
+ function resolveServerDataBackend(name, env = process.env) {
641
+ const databaseUrl = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
642
+ if (!databaseUrl) {
400
643
  return {
401
- mode: "local",
644
+ backend: "sqlite",
402
645
  source: "default",
403
- deprecatedAlias: null,
404
- databaseUrlPresent,
405
- databaseUrlSource,
406
- warning: null
646
+ databaseUrlPresent: false,
647
+ databaseUrlSource: null
407
648
  };
408
649
  }
409
- const { mode, deprecatedAlias } = normalizeStorageMode(modeHit.value);
410
- const warnings = [];
411
- if (deprecatedAlias) {
412
- warnings.push(`Deprecated storage mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Set ${modeKeys[0]}=cloud instead.`);
413
- }
414
- if (mode === "cloud" && !databaseUrlPresent) {
415
- warnings.push(`cloud mode needs ${databaseUrlKeys[0]} (PURE REMOTE: reads and writes go to cloud Postgres).`);
416
- }
417
- if (modeHit.key !== modeKeys[0]) {
418
- warnings.push(`Using alias env ${modeHit.key}; the canonical key is ${modeKeys[0]}.`);
419
- }
420
650
  return {
421
- mode,
422
- source: modeHit.key,
423
- deprecatedAlias,
424
- databaseUrlPresent,
425
- databaseUrlSource,
426
- warning: warnings.length > 0 ? warnings.join(" ") : null
651
+ backend: "postgresql",
652
+ source: databaseUrl.key,
653
+ databaseUrlPresent: true,
654
+ databaseUrlSource: databaseUrl.key
427
655
  };
428
656
  }
429
657
  function resolveDatabaseUrl(name, env = process.env) {
430
- const { databaseUrlKeys } = storageEnvKeys(name);
431
- const hit = firstEnv(env, databaseUrlKeys);
432
- return hit ? hit.value : null;
658
+ const hit = firstEnv(env, serverDataBackendEnvKeys(name).databaseUrlKeys);
659
+ return hit?.value ?? null;
433
660
  }
434
661
  // src/generated/storage-kit/tls.ts
435
662
  import { readFileSync } from "fs";
436
- function sslModeFromConnectionString(connectionString) {
663
+ var PG_TLS_QUERY_PARAMETERS = new Set([
664
+ "ssl",
665
+ "sslmode",
666
+ "sslrootcert",
667
+ "sslcert",
668
+ "sslkey",
669
+ "sslpassword",
670
+ "sslnegotiation",
671
+ "uselibpqcompat"
672
+ ]);
673
+ var EXPLICIT_SSL_ON_VALUES = new Set(["1", "true", "yes", "on", "require"]);
674
+ var EXPLICIT_SSL_OFF_VALUES = new Set(["0", "false", "no", "off", "disable"]);
675
+ var SSLMODE_VALUES = new Map([
676
+ ["disable", "disable"],
677
+ ["allow", "prefer"],
678
+ ["prefer", "prefer"],
679
+ ["require", "require"],
680
+ ["verify-ca", "verify-ca"],
681
+ ["verify-full", "verify-full"]
682
+ ]);
683
+ function connectionStringParts(connectionString) {
437
684
  const queryStart = connectionString.indexOf("?");
438
- const params = new URLSearchParams(queryStart === -1 ? "" : connectionString.slice(queryStart + 1));
439
- const sslmode = params.get("sslmode")?.trim().toLowerCase();
440
- if (sslmode) {
441
- switch (sslmode) {
442
- case "disable":
443
- case "prefer":
444
- case "require":
445
- case "verify-ca":
446
- case "verify-full":
447
- return sslmode;
448
- case "allow":
449
- return "prefer";
450
- default:
451
- throw new Error(`Unknown sslmode '${sslmode}' in connection string.`);
685
+ if (queryStart === -1) {
686
+ return { base: connectionString, fragment: "", params: new URLSearchParams };
687
+ }
688
+ const base = connectionString.slice(0, queryStart);
689
+ const queryAndFragment = connectionString.slice(queryStart + 1);
690
+ const fragmentStart = queryAndFragment.indexOf("#");
691
+ const query = fragmentStart === -1 ? queryAndFragment : queryAndFragment.slice(0, fragmentStart);
692
+ const fragment = fragmentStart === -1 ? "" : queryAndFragment.slice(fragmentStart);
693
+ return { base, fragment, params: new URLSearchParams(query) };
694
+ }
695
+ function tlsQueryValues(connectionString) {
696
+ const values = new Map;
697
+ for (const [key, value] of connectionStringParts(connectionString).params) {
698
+ const normalized = key.toLowerCase();
699
+ if (PG_TLS_QUERY_PARAMETERS.has(normalized))
700
+ values.set(normalized, value);
701
+ }
702
+ return values;
703
+ }
704
+ function connectionStringWithoutTlsParameters(connectionString) {
705
+ const { base, fragment, params } = connectionStringParts(connectionString);
706
+ for (const key of [...params.keys()]) {
707
+ if (PG_TLS_QUERY_PARAMETERS.has(key.toLowerCase()))
708
+ params.delete(key);
709
+ }
710
+ const query = params.toString();
711
+ return `${base}${query ? `?${query}` : ""}${fragment}`;
712
+ }
713
+ function rawSslMode(values) {
714
+ const raw = values.get("sslmode");
715
+ return raw === undefined ? undefined : raw.trim().toLowerCase();
716
+ }
717
+ function sslNegotiationFromConnectionString(connectionString) {
718
+ const value = tlsQueryValues(connectionString).get("sslnegotiation")?.trim().toLowerCase();
719
+ if (!value)
720
+ return;
721
+ if (value === "postgres" || value === "direct")
722
+ return value;
723
+ throw new Error(`Unknown sslnegotiation '${value}' in connection string; expected postgres or direct.`);
724
+ }
725
+ function sslModeFromConnectionString(connectionString) {
726
+ const values = tlsQueryValues(connectionString);
727
+ const sslmode = rawSslMode(values);
728
+ if (sslmode !== undefined) {
729
+ const resolved = SSLMODE_VALUES.get(sslmode);
730
+ if (resolved)
731
+ return resolved;
732
+ throw new Error(`Unknown sslmode '${sslmode}' in connection string; expected one of ` + `${[...SSLMODE_VALUES.keys()].join(", ")}. Remove the parameter entirely to defer to ` + `PGSSLMODE \u2014 an empty value is not how that is spelled.`);
733
+ }
734
+ if (values.has("ssl")) {
735
+ const ssl = values.get("ssl")?.trim().toLowerCase() ?? "";
736
+ if (EXPLICIT_SSL_ON_VALUES.has(ssl))
737
+ return "require";
738
+ if (!EXPLICIT_SSL_OFF_VALUES.has(ssl)) {
739
+ throw new Error(`Unknown ssl value '${ssl}' in connection string.`);
452
740
  }
741
+ return "disable";
453
742
  }
454
- const ssl = params.get("ssl")?.trim().toLowerCase();
455
- if (ssl && ["1", "true", "yes", "on", "require"].includes(ssl))
743
+ const sslnegotiation = values.get("sslnegotiation")?.trim().toLowerCase();
744
+ if (sslnegotiation === "direct")
456
745
  return "require";
457
746
  return "disable";
458
747
  }
459
- function loadCaBundle(options) {
460
- const env = options.env ?? process.env;
461
- if (options.ca && options.ca.trim())
462
- return options.ca;
463
- const path = options.caCertPath ?? env.PGSSLROOTCERT ?? env.NODE_EXTRA_CA_CERTS;
748
+ function loadCaBundle(connectionString, options) {
749
+ const env = ownProp(options, "env") ?? process.env;
750
+ const ca = ownString(options, "ca");
751
+ if (ca && ca.trim())
752
+ return ca;
753
+ const sslRootCert = tlsQueryValues(connectionString).get("sslrootcert")?.trim();
754
+ const path = ownString(options, "caCertPath") ?? (sslRootCert ? sslRootCert : undefined) ?? ownString(env, "PGSSLROOTCERT") ?? ownString(env, "NODE_EXTRA_CA_CERTS");
464
755
  if (path && path.trim())
465
756
  return readFileSync(path.trim(), "utf8");
466
757
  return null;
467
758
  }
759
+ function loadClientCertificate(connectionString) {
760
+ const values = tlsQueryValues(connectionString);
761
+ const material = {};
762
+ const certPath = values.get("sslcert")?.trim();
763
+ if (certPath)
764
+ material.cert = readFileSync(certPath, "utf8");
765
+ const keyPath = values.get("sslkey")?.trim();
766
+ if (keyPath)
767
+ material.key = readFileSync(keyPath, "utf8");
768
+ const passphrase = values.get("sslpassword");
769
+ if (passphrase)
770
+ material.passphrase = passphrase;
771
+ return material;
772
+ }
468
773
  function resolveTlsConfig(connectionString, options = {}) {
469
774
  const mode = sslModeFromConnectionString(connectionString);
470
- if (mode === "disable" || mode === "prefer") {
471
- return;
472
- }
473
- const ca = loadCaBundle(options);
474
- if (mode === "require") {
475
- return ca ? { rejectUnauthorized: false, ca } : { rejectUnauthorized: false };
775
+ if (mode === "disable") {
776
+ const values = tlsQueryValues(connectionString);
777
+ const sslmode = rawSslMode(values);
778
+ const ssl = values.get("ssl")?.trim().toLowerCase();
779
+ const explicitlyOff = sslmode === "disable" || ssl !== undefined && EXPLICIT_SSL_OFF_VALUES.has(ssl);
780
+ return explicitlyOff ? false : undefined;
781
+ }
782
+ const ca = loadCaBundle(connectionString, options);
783
+ const clientCertificate = loadClientCertificate(connectionString);
784
+ if (mode === "prefer" || mode === "require") {
785
+ return { rejectUnauthorized: true, ...ca ? { ca } : {}, ...clientCertificate };
476
786
  }
477
787
  if (!ca) {
478
788
  throw new Error(`sslmode=${mode} requires a CA bundle. Set PGSSLROOTCERT (or pass caCertPath/ca) to the ` + `Amazon RDS global bundle: https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem`);
479
789
  }
480
- return { rejectUnauthorized: true, ca };
790
+ return { rejectUnauthorized: true, ca, ...clientCertificate };
481
791
  }
482
792
  // src/generated/storage-kit/query.ts
483
793
  function wrapExecutor(executor) {
@@ -534,44 +844,72 @@ function createQueryClient(pool) {
534
844
  }
535
845
  // src/generated/storage-kit/pool.ts
536
846
  import pg from "pg";
847
+ function ownPoolOptions(options) {
848
+ const own = Object.create(null);
849
+ const ca = ownString(options, "ca");
850
+ if (ca !== undefined)
851
+ own.ca = ca;
852
+ const caCertPath = ownString(options, "caCertPath");
853
+ if (caCertPath !== undefined)
854
+ own.caCertPath = caCertPath;
855
+ const env = ownProp(options, "env");
856
+ if (env !== undefined)
857
+ own.env = env;
858
+ const max = ownProp(options, "max");
859
+ if (max !== undefined)
860
+ own.max = max;
861
+ const idleTimeoutMillis = ownProp(options, "idleTimeoutMillis");
862
+ if (idleTimeoutMillis !== undefined)
863
+ own.idleTimeoutMillis = idleTimeoutMillis;
864
+ const connectionTimeoutMillis = ownProp(options, "connectionTimeoutMillis");
865
+ if (connectionTimeoutMillis !== undefined)
866
+ own.connectionTimeoutMillis = connectionTimeoutMillis;
867
+ const applicationName = ownString(options, "applicationName");
868
+ if (applicationName !== undefined)
869
+ own.applicationName = applicationName;
870
+ return own;
871
+ }
537
872
  function createPgPool(options) {
538
- const ssl = resolveTlsConfig(options.connectionString, {
539
- ...options.ca !== undefined ? { ca: options.ca } : {},
540
- ...options.caCertPath !== undefined ? { caCertPath: options.caCertPath } : {},
541
- ...options.env !== undefined ? { env: options.env } : {}
873
+ const connectionString = ownString(options, "connectionString");
874
+ if (!connectionString || !connectionString.trim()) {
875
+ throw new Error("createPgPool requires an own `connectionString` on the options object.");
876
+ }
877
+ const own = ownPoolOptions(options);
878
+ const ssl = resolveTlsConfig(connectionString, {
879
+ ...own.ca !== undefined ? { ca: own.ca } : {},
880
+ ...own.caCertPath !== undefined ? { caCertPath: own.caCertPath } : {},
881
+ ...own.env !== undefined ? { env: own.env } : {}
542
882
  });
543
- const config = { connectionString: options.connectionString };
883
+ const config = {
884
+ connectionString: connectionStringWithoutTlsParameters(connectionString)
885
+ };
544
886
  if (ssl !== undefined)
545
887
  config.ssl = ssl;
546
- if (options.max !== undefined)
547
- config.max = options.max;
548
- if (options.idleTimeoutMillis !== undefined)
549
- config.idleTimeoutMillis = options.idleTimeoutMillis;
550
- if (options.connectionTimeoutMillis !== undefined)
551
- config.connectionTimeoutMillis = options.connectionTimeoutMillis;
552
- if (options.applicationName !== undefined)
553
- config.application_name = options.applicationName;
888
+ const sslnegotiation = sslNegotiationFromConnectionString(connectionString);
889
+ if (sslnegotiation !== undefined)
890
+ config.sslnegotiation = sslnegotiation;
891
+ if (own.max !== undefined)
892
+ config.max = own.max;
893
+ if (own.idleTimeoutMillis !== undefined)
894
+ config.idleTimeoutMillis = own.idleTimeoutMillis;
895
+ if (own.connectionTimeoutMillis !== undefined)
896
+ config.connectionTimeoutMillis = own.connectionTimeoutMillis;
897
+ if (own.applicationName !== undefined)
898
+ config.application_name = own.applicationName;
554
899
  return new pg.Pool(config);
555
900
  }
556
- function createCloudPoolFromEnv(appName, options = {}) {
557
- const env = options.env ?? process.env;
558
- const resolution = resolveStorageMode(appName, env);
559
- if (resolution.mode !== "cloud") {
560
- throw new Error(`createCloudPoolFromEnv requires ${appName} storage mode 'cloud', got '${resolution.mode}'. ` + `Set HASNA_${appName.toUpperCase().replace(/-/g, "_")}_STORAGE_MODE=cloud.`);
561
- }
901
+ function createServerPoolFromEnv(appName, options = {}) {
902
+ const own = ownPoolOptions(options);
903
+ const env = own.env ?? process.env;
904
+ const resolution = resolveServerDataBackend(appName, env);
562
905
  const connectionString = resolveDatabaseUrl(appName, env);
563
906
  if (!connectionString) {
564
- throw new Error(`cloud mode for ${appName} needs a database URL. Set ` + `HASNA_${appName.toUpperCase().replace(/-/g, "_")}_DATABASE_URL.`);
907
+ throw new Error(`postgresql storage for ${appName} needs a database URL. Set ` + `HASNA_${appName.toUpperCase().replace(/-/g, "_")}_DATABASE_URL.`);
565
908
  }
566
909
  const pool = createPgPool({
910
+ ...own,
567
911
  connectionString,
568
- ...options.ca !== undefined ? { ca: options.ca } : {},
569
- ...options.caCertPath !== undefined ? { caCertPath: options.caCertPath } : {},
570
- env,
571
- ...options.max !== undefined ? { max: options.max } : {},
572
- ...options.idleTimeoutMillis !== undefined ? { idleTimeoutMillis: options.idleTimeoutMillis } : {},
573
- ...options.connectionTimeoutMillis !== undefined ? { connectionTimeoutMillis: options.connectionTimeoutMillis } : {},
574
- ...options.applicationName !== undefined ? { applicationName: options.applicationName } : {}
912
+ env
575
913
  });
576
914
  return {
577
915
  client: createQueryClient(pool),
@@ -693,7 +1031,8 @@ function rowToDomain(row) {
693
1031
  notes: row.notes,
694
1032
  metadata: parseJson(row.metadata, {}),
695
1033
  created_at: row.created_at,
696
- updated_at: row.updated_at
1034
+ updated_at: row.updated_at,
1035
+ expiry_synced_at: row.expiry_synced_at ?? null
697
1036
  };
698
1037
  }
699
1038
  function rowToDnsRecord(row) {
@@ -739,8 +1078,8 @@ class DomainsRepo {
739
1078
  id, name, registrar, status, registered_at, expires_at, auto_renew,
740
1079
  is_premium, premium_price, standard_price, purchase_price, purchase_date,
741
1080
  nameservers, whois, ssl_expires_at, ssl_issuer, notes, metadata,
742
- created_at, updated_at
743
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)
1081
+ created_at, updated_at, expiry_synced_at
1082
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)
744
1083
  RETURNING *`, [
745
1084
  id,
746
1085
  input.name.trim(),
@@ -761,7 +1100,8 @@ class DomainsRepo {
761
1100
  input.notes ?? null,
762
1101
  JSON.stringify(input.metadata ?? {}),
763
1102
  nowIso,
764
- nowIso
1103
+ nowIso,
1104
+ input.expiry_synced_at ?? null
765
1105
  ]);
766
1106
  return rowToDomain(row);
767
1107
  } catch (e) {
@@ -850,6 +1190,8 @@ class DomainsRepo {
850
1190
  setCol("notes", p["notes"] ?? null);
851
1191
  if ("metadata" in p)
852
1192
  setCol("metadata", JSON.stringify(p["metadata"] ?? {}));
1193
+ if ("expiry_synced_at" in p)
1194
+ setCol("expiry_synced_at", p["expiry_synced_at"] ?? null);
853
1195
  if (sets.length === 0)
854
1196
  return existing;
855
1197
  setCol("updated_at", new Date().toISOString());
@@ -880,7 +1222,15 @@ class DomainsRepo {
880
1222
  count(*) FILTER (
881
1223
  WHERE NULLIF(ssl_expires_at, '')::timestamptz
882
1224
  BETWEEN now() AND now() + interval '30 days'
883
- )::text AS ssl_expiring_30_days
1225
+ )::text AS ssl_expiring_30_days,
1226
+ count(*) FILTER (
1227
+ WHERE status = 'active'
1228
+ AND NULLIF(expires_at, '')::timestamptz < now()
1229
+ )::text AS past_expiry,
1230
+ count(*) FILTER (
1231
+ WHERE NULLIF(ssl_expires_at, '')::timestamptz < now()
1232
+ )::text AS ssl_past_expiry,
1233
+ count(*) FILTER (WHERE expiry_synced_at IS NULL)::text AS never_synced
884
1234
  FROM domains`);
885
1235
  const n = (k) => row && row[k] ? parseInt(row[k], 10) : 0;
886
1236
  return {
@@ -891,7 +1241,10 @@ class DomainsRepo {
891
1241
  redemption: n("redemption"),
892
1242
  auto_renew_enabled: n("auto_renew_enabled"),
893
1243
  expiring_30_days: n("expiring_30_days"),
894
- ssl_expiring_30_days: n("ssl_expiring_30_days")
1244
+ ssl_expiring_30_days: n("ssl_expiring_30_days"),
1245
+ past_expiry: n("past_expiry"),
1246
+ ssl_past_expiry: n("ssl_past_expiry"),
1247
+ never_synced: n("never_synced")
895
1248
  };
896
1249
  }
897
1250
  async listDnsRecords(domainId) {
@@ -1480,7 +1833,9 @@ var PG_MIGRATIONS = [
1480
1833
  `CREATE INDEX IF NOT EXISTS idx_domain_history_created ON domain_history(created_at)`,
1481
1834
  `CREATE INDEX IF NOT EXISTS idx_domain_history_email ON domain_history(registrant_email)`,
1482
1835
  `CREATE INDEX IF NOT EXISTS idx_domain_reputation_domain ON domain_reputation(domain_id)`,
1483
- `CREATE INDEX IF NOT EXISTS idx_domain_reputation_blacklisted ON domain_reputation(is_blacklisted)`
1836
+ `CREATE INDEX IF NOT EXISTS idx_domain_reputation_blacklisted ON domain_reputation(is_blacklisted)`,
1837
+ `ALTER TABLE domains ADD COLUMN IF NOT EXISTS expiry_synced_at TEXT`,
1838
+ `CREATE INDEX IF NOT EXISTS idx_domains_expiry_synced_at ON domains(expiry_synced_at)`
1484
1839
  ];
1485
1840
 
1486
1841
  // src/server/migrations.ts
@@ -1517,7 +1872,7 @@ function buildOpenApiSpec(version) {
1517
1872
  info: {
1518
1873
  title: "domains",
1519
1874
  version,
1520
- description: "Domain portfolio, registrar, marketplace, and DNS management HTTP API (self_hosted). API-key authenticated."
1875
+ description: "Domain portfolio, registrar, marketplace, and DNS management HTTP API. API-key authenticated."
1521
1876
  },
1522
1877
  security: [{ apiKey: [] }],
1523
1878
  paths: {
@@ -1671,29 +2026,26 @@ function buildOpenApiSpec(version) {
1671
2026
  properties: {
1672
2027
  status: { type: "string" },
1673
2028
  version: { type: "string" },
1674
- mode: { type: "string" },
1675
2029
  latencyMs: { type: "number" }
1676
2030
  },
1677
- required: ["status", "version", "mode"]
2031
+ required: ["status", "version"]
1678
2032
  },
1679
2033
  ReadyResponse: {
1680
2034
  type: "object",
1681
2035
  properties: {
1682
2036
  status: { type: "string" },
1683
2037
  version: { type: "string" },
1684
- mode: { type: "string" },
1685
2038
  pendingMigrations: { type: "array", items: { type: "string" } }
1686
2039
  },
1687
- required: ["status", "version", "mode"]
2040
+ required: ["status", "version"]
1688
2041
  },
1689
2042
  VersionResponse: {
1690
2043
  type: "object",
1691
2044
  properties: {
1692
2045
  status: { type: "string" },
1693
- version: { type: "string" },
1694
- mode: { type: "string" }
2046
+ version: { type: "string" }
1695
2047
  },
1696
- required: ["status", "version", "mode"]
2048
+ required: ["status", "version"]
1697
2049
  },
1698
2050
  DeleteResult: {
1699
2051
  type: "object",
@@ -1891,14 +2243,13 @@ function json(data, status = 200) {
1891
2243
  }
1892
2244
  function createServeApp(options) {
1893
2245
  const { db, version } = options;
1894
- const mode2 = options.mode ?? "self_hosted";
1895
2246
  const repo = new DomainsRepo(db);
1896
2247
  const migrationIds = buildMigrations().map((m) => m.id);
1897
2248
  const spec = buildOpenApiSpec(version);
1898
2249
  const verifier = verifyApiKey({
1899
2250
  app: "domains",
1900
2251
  signingSecret: options.signingSecret,
1901
- ...options.isRevoked ? { isRevoked: options.isRevoked } : {},
2252
+ keyStatus: options.keyStatus,
1902
2253
  ...options.audit ? { audit: options.audit } : {}
1903
2254
  });
1904
2255
  async function readBody(req) {
@@ -1928,20 +2279,19 @@ function createServeApp(options) {
1928
2279
  try {
1929
2280
  if (method === "GET" && path === "/health") {
1930
2281
  const h = await checkHealth(db);
1931
- return json({ status: h.ok ? "ok" : "error", version, mode: mode2, latencyMs: h.latencyMs, ...h.error ? { error: h.error } : {} }, h.ok ? 200 : 503);
2282
+ return json({ status: h.ok ? "ok" : "error", version, latencyMs: h.latencyMs, ...h.error ? { error: h.error } : {} }, h.ok ? 200 : 503);
1932
2283
  }
1933
2284
  if (method === "GET" && path === "/ready") {
1934
2285
  const r = await readReadiness(db, migrationIds);
1935
2286
  return json({
1936
2287
  status: r.ok ? "ok" : "not_ready",
1937
2288
  version,
1938
- mode: mode2,
1939
2289
  pendingMigrations: r.pendingMigrations,
1940
2290
  ...r.error ? { error: r.error } : {}
1941
2291
  }, r.ok ? 200 : 503);
1942
2292
  }
1943
2293
  if (method === "GET" && (path === "/version" || path === "/v1/version")) {
1944
- return json({ status: "ok", version, mode: mode2 });
2294
+ return json({ status: "ok", version });
1945
2295
  }
1946
2296
  if (method === "GET" && (path === "/openapi.json" || path === "/v1/openapi.json")) {
1947
2297
  return json(spec);
@@ -2312,9 +2662,6 @@ function normalizeEnv(env = process.env) {
2312
2662
  if (!env["HASNA_DOMAINS_DATABASE_URL"] && env["DATABASE_URL"]) {
2313
2663
  env["HASNA_DOMAINS_DATABASE_URL"] = env["DATABASE_URL"];
2314
2664
  }
2315
- if (!env["HASNA_DOMAINS_STORAGE_MODE"] && env["HASNA_DOMAINS_DATABASE_URL"]) {
2316
- env["HASNA_DOMAINS_STORAGE_MODE"] = "cloud";
2317
- }
2318
2665
  }
2319
2666
  function resolveSigningSecret(env = process.env) {
2320
2667
  for (const key of SIGNING_KEY_ENVS) {
@@ -2341,7 +2688,7 @@ async function main() {
2341
2688
  normalizeEnv();
2342
2689
  const version = getPackageVersion();
2343
2690
  const signingSecret = resolveSigningSecret();
2344
- const { client, connectionSource } = createCloudPoolFromEnv("domains", {
2691
+ const { client, connectionSource } = createServerPoolFromEnv("domains", {
2345
2692
  applicationName: "domains-serve",
2346
2693
  max: 5
2347
2694
  });
@@ -2350,8 +2697,7 @@ async function main() {
2350
2697
  db: client,
2351
2698
  signingSecret,
2352
2699
  version,
2353
- mode: process.env["HASNA_APP_MODE"] ?? "self_hosted",
2354
- isRevoked: store.isRevoked,
2700
+ keyStatus: store.keyStatus,
2355
2701
  audit: (e) => {
2356
2702
  if (e.outcome === "deny") {
2357
2703
  console.error(JSON.stringify({ level: "warn", event: "api_auth_deny", ...e }));