@hasna/contacts 0.7.0 → 0.8.1

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 (39) hide show
  1. package/README.md +83 -13
  2. package/dist/cli/commands/core.d.ts.map +1 -1
  3. package/dist/cli/index.js +82 -33
  4. package/dist/cloud/http-storage.d.ts +20 -12
  5. package/dist/cloud/http-storage.d.ts.map +1 -1
  6. package/dist/cloud/resolver-inputs.d.ts +51 -0
  7. package/dist/cloud/resolver-inputs.d.ts.map +1 -0
  8. package/dist/db/paths.d.ts +4 -4
  9. package/dist/db/paths.d.ts.map +1 -1
  10. package/dist/generated/storage-kit/backend.d.ts +19 -0
  11. package/dist/generated/storage-kit/backend.d.ts.map +1 -0
  12. package/dist/generated/storage-kit/index.d.ts +2 -1
  13. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  14. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  15. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  16. package/dist/generated/storage-kit/own.d.ts +11 -0
  17. package/dist/generated/storage-kit/own.d.ts.map +1 -0
  18. package/dist/generated/storage-kit/pool.d.ts +5 -17
  19. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  20. package/dist/generated/storage-kit/query.d.ts +1 -1
  21. package/dist/generated/storage-kit/query.d.ts.map +1 -1
  22. package/dist/generated/storage-kit/tls.d.ts +30 -3
  23. package/dist/generated/storage-kit/tls.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +105 -24
  27. package/dist/mcp/index.d.ts +9 -0
  28. package/dist/mcp/index.d.ts.map +1 -1
  29. package/dist/mcp/index.js +112 -18
  30. package/dist/mcp/startup-gate.d.ts +48 -0
  31. package/dist/mcp/startup-gate.d.ts.map +1 -0
  32. package/dist/sdk/index.d.ts +70 -3
  33. package/dist/sdk/index.d.ts.map +1 -1
  34. package/dist/sdk/index.js +990 -7
  35. package/dist/server/index.js +201 -43
  36. package/hasna.contract.json +20 -6
  37. package/package.json +3 -3
  38. package/dist/lib/config.d.ts +0 -7
  39. package/dist/lib/config.d.ts.map +0 -1
@@ -36,53 +36,152 @@ import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
36
36
  // src/generated/storage-kit/pool.ts
37
37
  import pg from "pg";
38
38
 
39
+ // src/generated/storage-kit/own.ts
40
+ function ownProp(source, key) {
41
+ if (source === null || source === undefined)
42
+ return;
43
+ const kind = typeof source;
44
+ if (kind !== "object" && kind !== "function")
45
+ return;
46
+ if (!Object.hasOwn(source, key))
47
+ return;
48
+ return source[key];
49
+ }
50
+ function ownString(source, key) {
51
+ const value = ownProp(source, key);
52
+ return typeof value === "string" ? value : undefined;
53
+ }
54
+
39
55
  // src/generated/storage-kit/tls.ts
40
56
  import { readFileSync as readFileSync2 } from "fs";
41
- function sslModeFromConnectionString(connectionString) {
57
+ var PG_TLS_QUERY_PARAMETERS = new Set([
58
+ "ssl",
59
+ "sslmode",
60
+ "sslrootcert",
61
+ "sslcert",
62
+ "sslkey",
63
+ "sslpassword",
64
+ "sslnegotiation",
65
+ "uselibpqcompat"
66
+ ]);
67
+ var EXPLICIT_SSL_ON_VALUES = new Set(["1", "true", "yes", "on", "require"]);
68
+ var EXPLICIT_SSL_OFF_VALUES = new Set(["0", "false", "no", "off", "disable"]);
69
+ var SSLMODE_VALUES = new Map([
70
+ ["disable", "disable"],
71
+ ["allow", "prefer"],
72
+ ["prefer", "prefer"],
73
+ ["require", "require"],
74
+ ["verify-ca", "verify-ca"],
75
+ ["verify-full", "verify-full"]
76
+ ]);
77
+ function connectionStringParts(connectionString) {
42
78
  const queryStart = connectionString.indexOf("?");
43
- const params = new URLSearchParams(queryStart === -1 ? "" : connectionString.slice(queryStart + 1));
44
- const sslmode = params.get("sslmode")?.trim().toLowerCase();
45
- if (sslmode) {
46
- switch (sslmode) {
47
- case "disable":
48
- case "prefer":
49
- case "require":
50
- case "verify-ca":
51
- case "verify-full":
52
- return sslmode;
53
- case "allow":
54
- return "prefer";
55
- default:
56
- throw new Error(`Unknown sslmode '${sslmode}' in connection string.`);
79
+ if (queryStart === -1) {
80
+ return { base: connectionString, fragment: "", params: new URLSearchParams };
81
+ }
82
+ const base = connectionString.slice(0, queryStart);
83
+ const queryAndFragment = connectionString.slice(queryStart + 1);
84
+ const fragmentStart = queryAndFragment.indexOf("#");
85
+ const query = fragmentStart === -1 ? queryAndFragment : queryAndFragment.slice(0, fragmentStart);
86
+ const fragment = fragmentStart === -1 ? "" : queryAndFragment.slice(fragmentStart);
87
+ return { base, fragment, params: new URLSearchParams(query) };
88
+ }
89
+ function tlsQueryValues(connectionString) {
90
+ const values = new Map;
91
+ for (const [key, value] of connectionStringParts(connectionString).params) {
92
+ const normalized = key.toLowerCase();
93
+ if (PG_TLS_QUERY_PARAMETERS.has(normalized))
94
+ values.set(normalized, value);
95
+ }
96
+ return values;
97
+ }
98
+ function connectionStringWithoutTlsParameters(connectionString) {
99
+ const { base, fragment, params } = connectionStringParts(connectionString);
100
+ for (const key of [...params.keys()]) {
101
+ if (PG_TLS_QUERY_PARAMETERS.has(key.toLowerCase()))
102
+ params.delete(key);
103
+ }
104
+ const query = params.toString();
105
+ return `${base}${query ? `?${query}` : ""}${fragment}`;
106
+ }
107
+ function rawSslMode(values) {
108
+ const raw = values.get("sslmode");
109
+ return raw === undefined ? undefined : raw.trim().toLowerCase();
110
+ }
111
+ function sslNegotiationFromConnectionString(connectionString) {
112
+ const value = tlsQueryValues(connectionString).get("sslnegotiation")?.trim().toLowerCase();
113
+ if (!value)
114
+ return;
115
+ if (value === "postgres" || value === "direct")
116
+ return value;
117
+ throw new Error(`Unknown sslnegotiation '${value}' in connection string; expected postgres or direct.`);
118
+ }
119
+ function sslModeFromConnectionString(connectionString) {
120
+ const values = tlsQueryValues(connectionString);
121
+ const sslmode = rawSslMode(values);
122
+ if (sslmode !== undefined) {
123
+ const resolved = SSLMODE_VALUES.get(sslmode);
124
+ if (resolved)
125
+ return resolved;
126
+ 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.`);
127
+ }
128
+ if (values.has("ssl")) {
129
+ const ssl = values.get("ssl")?.trim().toLowerCase() ?? "";
130
+ if (EXPLICIT_SSL_ON_VALUES.has(ssl))
131
+ return "require";
132
+ if (!EXPLICIT_SSL_OFF_VALUES.has(ssl)) {
133
+ throw new Error(`Unknown ssl value '${ssl}' in connection string.`);
57
134
  }
135
+ return "disable";
58
136
  }
59
- const ssl = params.get("ssl")?.trim().toLowerCase();
60
- if (ssl && ["1", "true", "yes", "on", "require"].includes(ssl))
137
+ const sslnegotiation = values.get("sslnegotiation")?.trim().toLowerCase();
138
+ if (sslnegotiation === "direct")
61
139
  return "require";
62
140
  return "disable";
63
141
  }
64
- function loadCaBundle(options) {
65
- const env = options.env ?? process.env;
66
- if (options.ca && options.ca.trim())
67
- return options.ca;
68
- const path = options.caCertPath ?? env.PGSSLROOTCERT ?? env.NODE_EXTRA_CA_CERTS;
142
+ function loadCaBundle(connectionString, options) {
143
+ const env = ownProp(options, "env") ?? process.env;
144
+ const ca = ownString(options, "ca");
145
+ if (ca && ca.trim())
146
+ return ca;
147
+ const sslRootCert = tlsQueryValues(connectionString).get("sslrootcert")?.trim();
148
+ const path = ownString(options, "caCertPath") ?? (sslRootCert ? sslRootCert : undefined) ?? ownString(env, "PGSSLROOTCERT") ?? ownString(env, "NODE_EXTRA_CA_CERTS");
69
149
  if (path && path.trim())
70
150
  return readFileSync2(path.trim(), "utf8");
71
151
  return null;
72
152
  }
153
+ function loadClientCertificate(connectionString) {
154
+ const values = tlsQueryValues(connectionString);
155
+ const material = {};
156
+ const certPath = values.get("sslcert")?.trim();
157
+ if (certPath)
158
+ material.cert = readFileSync2(certPath, "utf8");
159
+ const keyPath = values.get("sslkey")?.trim();
160
+ if (keyPath)
161
+ material.key = readFileSync2(keyPath, "utf8");
162
+ const passphrase = values.get("sslpassword");
163
+ if (passphrase)
164
+ material.passphrase = passphrase;
165
+ return material;
166
+ }
73
167
  function resolveTlsConfig(connectionString, options = {}) {
74
168
  const mode = sslModeFromConnectionString(connectionString);
75
- if (mode === "disable" || mode === "prefer") {
76
- return;
77
- }
78
- const ca = loadCaBundle(options);
79
- if (mode === "require") {
80
- return ca ? { rejectUnauthorized: false, ca } : { rejectUnauthorized: false };
169
+ if (mode === "disable") {
170
+ const values = tlsQueryValues(connectionString);
171
+ const sslmode = rawSslMode(values);
172
+ const ssl = values.get("ssl")?.trim().toLowerCase();
173
+ const explicitlyOff = sslmode === "disable" || ssl !== undefined && EXPLICIT_SSL_OFF_VALUES.has(ssl);
174
+ return explicitlyOff ? false : undefined;
175
+ }
176
+ const ca = loadCaBundle(connectionString, options);
177
+ const clientCertificate = loadClientCertificate(connectionString);
178
+ if (mode === "prefer" || mode === "require") {
179
+ return { rejectUnauthorized: true, ...ca ? { ca } : {}, ...clientCertificate };
81
180
  }
82
181
  if (!ca) {
83
182
  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`);
84
183
  }
85
- return { rejectUnauthorized: true, ca };
184
+ return { rejectUnauthorized: true, ca, ...clientCertificate };
86
185
  }
87
186
 
88
187
  // src/generated/storage-kit/query.ts
@@ -140,23 +239,58 @@ function createQueryClient(pool) {
140
239
  }
141
240
 
142
241
  // src/generated/storage-kit/pool.ts
242
+ function ownPoolOptions(options) {
243
+ const own = Object.create(null);
244
+ const ca = ownString(options, "ca");
245
+ if (ca !== undefined)
246
+ own.ca = ca;
247
+ const caCertPath = ownString(options, "caCertPath");
248
+ if (caCertPath !== undefined)
249
+ own.caCertPath = caCertPath;
250
+ const env = ownProp(options, "env");
251
+ if (env !== undefined)
252
+ own.env = env;
253
+ const max = ownProp(options, "max");
254
+ if (max !== undefined)
255
+ own.max = max;
256
+ const idleTimeoutMillis = ownProp(options, "idleTimeoutMillis");
257
+ if (idleTimeoutMillis !== undefined)
258
+ own.idleTimeoutMillis = idleTimeoutMillis;
259
+ const connectionTimeoutMillis = ownProp(options, "connectionTimeoutMillis");
260
+ if (connectionTimeoutMillis !== undefined)
261
+ own.connectionTimeoutMillis = connectionTimeoutMillis;
262
+ const applicationName = ownString(options, "applicationName");
263
+ if (applicationName !== undefined)
264
+ own.applicationName = applicationName;
265
+ return own;
266
+ }
143
267
  function createPgPool(options) {
144
- const ssl = resolveTlsConfig(options.connectionString, {
145
- ...options.ca !== undefined ? { ca: options.ca } : {},
146
- ...options.caCertPath !== undefined ? { caCertPath: options.caCertPath } : {},
147
- ...options.env !== undefined ? { env: options.env } : {}
268
+ const connectionString = ownString(options, "connectionString");
269
+ if (!connectionString || !connectionString.trim()) {
270
+ throw new Error("createPgPool requires an own `connectionString` on the options object.");
271
+ }
272
+ const own = ownPoolOptions(options);
273
+ const ssl = resolveTlsConfig(connectionString, {
274
+ ...own.ca !== undefined ? { ca: own.ca } : {},
275
+ ...own.caCertPath !== undefined ? { caCertPath: own.caCertPath } : {},
276
+ ...own.env !== undefined ? { env: own.env } : {}
148
277
  });
149
- const config = { connectionString: options.connectionString };
278
+ const config = {
279
+ connectionString: connectionStringWithoutTlsParameters(connectionString)
280
+ };
150
281
  if (ssl !== undefined)
151
282
  config.ssl = ssl;
152
- if (options.max !== undefined)
153
- config.max = options.max;
154
- if (options.idleTimeoutMillis !== undefined)
155
- config.idleTimeoutMillis = options.idleTimeoutMillis;
156
- if (options.connectionTimeoutMillis !== undefined)
157
- config.connectionTimeoutMillis = options.connectionTimeoutMillis;
158
- if (options.applicationName !== undefined)
159
- config.application_name = options.applicationName;
283
+ const sslnegotiation = sslNegotiationFromConnectionString(connectionString);
284
+ if (sslnegotiation !== undefined)
285
+ config.sslnegotiation = sslnegotiation;
286
+ if (own.max !== undefined)
287
+ config.max = own.max;
288
+ if (own.idleTimeoutMillis !== undefined)
289
+ config.idleTimeoutMillis = own.idleTimeoutMillis;
290
+ if (own.connectionTimeoutMillis !== undefined)
291
+ config.connectionTimeoutMillis = own.connectionTimeoutMillis;
292
+ if (own.applicationName !== undefined)
293
+ config.application_name = own.applicationName;
160
294
  return new pg.Pool(config);
161
295
  }
162
296
 
@@ -4461,8 +4595,32 @@ async function runMigrate() {
4461
4595
  console.log("migrate: done");
4462
4596
  await closeCloud();
4463
4597
  }
4598
+ function handleEarlyArgs(argv) {
4599
+ if (argv.includes("--help") || argv.includes("-h"))
4600
+ return "help";
4601
+ if (argv.includes("--version") || argv.includes("-V"))
4602
+ return "version";
4603
+ return "start";
4604
+ }
4605
+ function usage() {
4606
+ return `usage: contacts-serve [--port <n>] [--host <h>] Start the authenticated /v1 HTTP API over PostgreSQL
4607
+ contacts-serve migrate Apply the PostgreSQL schema and exit
4608
+ contacts-serve --version Print the version
4609
+
4610
+ options:
4611
+ --port <n> listen port (default ${DEFAULT_PORT}; PORT)
4612
+ --host <h> bind host (default ${DEFAULT_HOST}; CONTACTS_HOST)
4613
+ --help, -h show this help and exit
4614
+ --version, -V print the package version and exit
4615
+ `;
4616
+ }
4464
4617
  async function main() {
4465
- if (process.argv.includes("--version") || process.argv.includes("-V")) {
4618
+ const early = handleEarlyArgs(process.argv.slice(2));
4619
+ if (early === "help") {
4620
+ console.log(usage());
4621
+ return;
4622
+ }
4623
+ if (early === "version") {
4466
4624
  console.log(getPackageVersion());
4467
4625
  return;
4468
4626
  }
@@ -4,8 +4,8 @@
4
4
  "name": "contacts",
5
5
  "class": "service",
6
6
  "contractVersion": "v1",
7
- "kitVersion": "0.14.2",
8
- "description": "Contact and company management for AI coding agents. Public CLI, MCP, and SDK clients use one explicitly configured authenticated HTTPS /v1 authority and never open SQLite or PostgreSQL. contacts-serve is the server-only PostgreSQL/API-key boundary.",
7
+ "kitVersion": "1.0.2",
8
+ "description": "Contact and company management for AI coding agents. Public CLI, MCP, and SDK clients use one authenticated HTTPS /v1 authority resolved by the @hasna/contracts client chain (fleet gateway default https://api.hasna.com/contacts) and never open SQLite or PostgreSQL. contacts-serve is the server-only PostgreSQL/API-key boundary.",
9
9
  "bins": [
10
10
  "contacts",
11
11
  "contacts-mcp",
@@ -22,9 +22,21 @@
22
22
  "status": "supported",
23
23
  "bin": "contacts-serve",
24
24
  "authMode": "api-key",
25
- "health": { "method": "GET", "path": "/health", "public": true },
26
- "readiness": { "method": "GET", "path": "/ready", "public": true },
27
- "version": { "method": "GET", "path": "/version", "public": true },
25
+ "health": {
26
+ "method": "GET",
27
+ "path": "/health",
28
+ "public": true
29
+ },
30
+ "readiness": {
31
+ "method": "GET",
32
+ "path": "/ready",
33
+ "public": true
34
+ },
35
+ "version": {
36
+ "method": "GET",
37
+ "path": "/version",
38
+ "public": true
39
+ },
28
40
  "apiBasePath": "/v1",
29
41
  "openApiPath": "/openapi.json"
30
42
  },
@@ -54,7 +66,9 @@
54
66
  ],
55
67
  "storage": {
56
68
  "backend": "postgresql",
57
- "engines": ["postgresql"],
69
+ "engines": [
70
+ "postgresql"
71
+ ],
58
72
  "envPrefix": "HASNA_CONTACTS_",
59
73
  "aliasEnvPrefix": "CONTACTS_",
60
74
  "pgTestGate": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.7.0",
4
- "description": "Contact management for AI coding agents \u2014 CLI + MCP + Web",
3
+ "version": "0.8.1",
4
+ "description": "Contact management for AI coding agents CLI + MCP + Web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -75,7 +75,7 @@
75
75
  "author": "Andrei Hasna <andrei@hasna.com>",
76
76
  "license": "Apache-2.0",
77
77
  "dependencies": {
78
- "@hasna/contracts": "0.14.2",
78
+ "@hasna/contracts": "1.0.2",
79
79
  "@hasna/events": "^0.1.6",
80
80
  "chalk": "^5.4.1",
81
81
  "commander": "^13.1.0",
@@ -1,7 +0,0 @@
1
- interface ContactsConfig {
2
- db_path?: string;
3
- }
4
- export declare function readConfig(): ContactsConfig;
5
- export declare function writeConfig(config: ContactsConfig): void;
6
- export {};
7
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,cAAc,CAG3C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAGxD"}