@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.
- package/README.md +83 -13
- package/dist/cli/commands/core.d.ts.map +1 -1
- package/dist/cli/index.js +82 -33
- package/dist/cloud/http-storage.d.ts +20 -12
- package/dist/cloud/http-storage.d.ts.map +1 -1
- package/dist/cloud/resolver-inputs.d.ts +51 -0
- package/dist/cloud/resolver-inputs.d.ts.map +1 -0
- package/dist/db/paths.d.ts +4 -4
- package/dist/db/paths.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +19 -0
- package/dist/generated/storage-kit/backend.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +2 -1
- package/dist/generated/storage-kit/index.d.ts.map +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/generated/storage-kit/own.d.ts +11 -0
- package/dist/generated/storage-kit/own.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +5 -17
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/query.d.ts +1 -1
- package/dist/generated/storage-kit/query.d.ts.map +1 -1
- package/dist/generated/storage-kit/tls.d.ts +30 -3
- package/dist/generated/storage-kit/tls.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -24
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +112 -18
- package/dist/mcp/startup-gate.d.ts +48 -0
- package/dist/mcp/startup-gate.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +70 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +990 -7
- package/dist/server/index.js +201 -43
- package/hasna.contract.json +20 -6
- package/package.json +3 -3
- package/dist/lib/config.d.ts +0 -7
- package/dist/lib/config.d.ts.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
if (
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 = {
|
|
278
|
+
const config = {
|
|
279
|
+
connectionString: connectionStringWithoutTlsParameters(connectionString)
|
|
280
|
+
};
|
|
150
281
|
if (ssl !== undefined)
|
|
151
282
|
config.ssl = ssl;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
}
|
package/hasna.contract.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"name": "contacts",
|
|
5
5
|
"class": "service",
|
|
6
6
|
"contractVersion": "v1",
|
|
7
|
-
"kitVersion": "0.
|
|
8
|
-
"description": "Contact and company management for AI coding agents. Public CLI, MCP, and SDK clients use one
|
|
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": {
|
|
26
|
-
|
|
27
|
-
|
|
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": [
|
|
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.
|
|
4
|
-
"description": "Contact management for AI coding agents
|
|
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.
|
|
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",
|
package/dist/lib/config.d.ts
DELETED
package/dist/lib/config.d.ts.map
DELETED
|
@@ -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"}
|