@semiont/gateway 0.5.28
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 +54 -0
- package/dist/cli/db-url.js +45 -0
- package/dist/cli/db-url.js.map +1 -0
- package/dist/cli/useradd.js +224 -0
- package/dist/cli/useradd.js.map +1 -0
- package/dist/index.js +12120 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.json +7967 -0
- package/package.json +45 -0
- package/prisma/migrations/20250910180513_init/migration.sql +24 -0
- package/prisma/migrations/20250912124942_add_moderator_role/migration.sql +2 -0
- package/prisma/migrations/20251129131237_add_password_auth/migration.sql +2 -0
- package/prisma/migrations/20260619120000_add_token_version/migration.sql +3 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +41 -0
- package/prisma.config.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @semiont/gateway
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@semiont/gateway)
|
|
4
|
+
[](https://www.npmjs.com/package/@semiont/gateway)
|
|
5
|
+
[](https://github.com/The-AI-Alliance/semiont/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
Pre-built Semiont gateway server for npm consumption. This package contains the compiled gateway application with Prisma schema and migrations.
|
|
8
|
+
|
|
9
|
+
## Running Semiont
|
|
10
|
+
|
|
11
|
+
Most people should **not** install this package directly. A Semiont stack is run with the `semiont`
|
|
12
|
+
launcher — a single static binary that pulls the published container images:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
brew install the-ai-alliance/semiont/semiont
|
|
16
|
+
|
|
17
|
+
cd /path/to/your-knowledge-base
|
|
18
|
+
semiont start
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This package is what the `semiont-gateway` container image runs inside.
|
|
22
|
+
|
|
23
|
+
## Direct usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @semiont/gateway
|
|
27
|
+
|
|
28
|
+
npx prisma migrate deploy --schema=node_modules/@semiont/gateway/prisma/schema.prisma
|
|
29
|
+
node node_modules/@semiont/gateway/dist/index.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires in the environment:
|
|
33
|
+
|
|
34
|
+
- `SEMIONT_ROOT` — path to the knowledge-base working tree
|
|
35
|
+
- A config TOML readable at `~/.semiontconfig` — the container image mounts the
|
|
36
|
+
KB's `.semiont/semiontconfig/<name>.toml` there; running the package directly
|
|
37
|
+
means placing or symlinking one yourself. The environment block comes from its
|
|
38
|
+
`[defaults] environment`.
|
|
39
|
+
- `DATABASE_URL` — Postgres connection string. The container image derives one
|
|
40
|
+
from `services.database` in the config when this is unset; running the package
|
|
41
|
+
directly has no such step, so set it yourself.
|
|
42
|
+
- `JWT_SECRET` — minimum 32 characters
|
|
43
|
+
- `SEMIONT_WORKER_SECRET` — for the software-agent token exchange
|
|
44
|
+
|
|
45
|
+
## What's included
|
|
46
|
+
|
|
47
|
+
- `dist/` — compiled gateway application (Hono server)
|
|
48
|
+
- `prisma/` — Prisma schema and migrations
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [Semiont GitHub](https://github.com/The-AI-Alliance/semiont)
|
|
53
|
+
- [Semiont launcher](https://github.com/The-AI-Alliance/semiont/tree/main/apps/launcher)
|
|
54
|
+
- [Documentation](https://github.com/The-AI-Alliance/semiont#readme)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadEnvironmentConfig } from '@semiont/core/node';
|
|
3
|
+
|
|
4
|
+
// src/utils/database-url.ts
|
|
5
|
+
function databaseUrlFrom(config) {
|
|
6
|
+
const db = config.services?.database;
|
|
7
|
+
if (!db) {
|
|
8
|
+
throw new Error("services.database is required in environment config to derive DATABASE_URL");
|
|
9
|
+
}
|
|
10
|
+
const name = db.name ?? db.database;
|
|
11
|
+
const user = db.user ?? db.username;
|
|
12
|
+
const missing = [
|
|
13
|
+
["host", db.host],
|
|
14
|
+
["port", db.port],
|
|
15
|
+
["name", name],
|
|
16
|
+
["user", user],
|
|
17
|
+
["password", db.password]
|
|
18
|
+
].filter(([, v]) => v === void 0 || v === null || v === "").map(([k]) => k);
|
|
19
|
+
if (missing.length > 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`services.database is missing ${missing.join(", ")} \u2014 needed to derive DATABASE_URL`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const url = new URL("postgresql://placeholder");
|
|
25
|
+
url.username = String(user);
|
|
26
|
+
url.password = String(db.password);
|
|
27
|
+
url.hostname = String(db.host);
|
|
28
|
+
url.port = String(db.port);
|
|
29
|
+
url.pathname = `/${name}`;
|
|
30
|
+
return url.toString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/cli/db-url.ts
|
|
34
|
+
try {
|
|
35
|
+
process.stdout.write(databaseUrlFrom(loadEnvironmentConfig(null)));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
`Could not derive DATABASE_URL from config: ${error instanceof Error ? error.message : String(error)}
|
|
39
|
+
Set DATABASE_URL explicitly to bypass this derivation.
|
|
40
|
+
`
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=db-url.js.map
|
|
45
|
+
//# sourceMappingURL=db-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/database-url.ts","../../src/cli/db-url.ts"],"names":[],"mappings":";;;;AAuBO,SAAS,gBAAgB,MAAA,EAAmC;AACjE,EAAA,MAAM,EAAA,GAAK,OAAO,QAAA,EAAU,QAAA;AAC5B,EAAA,IAAI,CAAC,EAAA,EAAI;AACP,IAAA,MAAM,IAAI,MAAM,4EAA4E,CAAA;AAAA,EAC9F;AAIA,EAAA,MAAM,IAAA,GAAO,EAAA,CAAG,IAAA,IAAQ,EAAA,CAAG,QAAA;AAC3B,EAAA,MAAM,IAAA,GAAO,EAAA,CAAG,IAAA,IAAQ,EAAA,CAAG,QAAA;AAC3B,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,CAAC,MAAA,EAAQ,EAAA,CAAG,IAAI,CAAA;AAAA,IAChB,CAAC,MAAA,EAAQ,EAAA,CAAG,IAAI,CAAA;AAAA,IAChB,CAAC,QAAQ,IAAI,CAAA;AAAA,IACb,CAAC,QAAQ,IAAI,CAAA;AAAA,IACb,CAAC,UAAA,EAAY,EAAA,CAAG,QAAQ;AAAA,IACxB,MAAA,CAAO,CAAC,GAAG,CAAC,MAAM,CAAA,KAAM,MAAA,IAAa,MAAM,IAAA,IAAQ,CAAA,KAAM,EAAE,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7E,EAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,6BAAA,EAAgC,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA,qCAAA;AAAA,KACpD;AAAA,EACF;AAMA,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,0BAA0B,CAAA;AAC9C,EAAA,GAAA,CAAI,QAAA,GAAW,OAAO,IAAI,CAAA;AAC1B,EAAA,GAAA,CAAI,QAAA,GAAW,MAAA,CAAO,EAAA,CAAG,QAAQ,CAAA;AACjC,EAAA,GAAA,CAAI,QAAA,GAAW,MAAA,CAAO,EAAA,CAAG,IAAI,CAAA;AAC7B,EAAA,GAAA,CAAI,IAAA,GAAO,MAAA,CAAO,EAAA,CAAG,IAAI,CAAA;AACzB,EAAA,GAAA,CAAI,QAAA,GAAW,IAAI,IAAI,CAAA,CAAA;AAMvB,EAAA,OAAO,IAAI,QAAA,EAAS;AACtB;;;ACnBA,IAAI;AACF,EAAA,OAAA,CAAQ,OAAO,KAAA,CAAM,eAAA,CAAgB,qBAAA,CAAsB,IAAI,CAAC,CAAC,CAAA;AACnE,CAAA,CAAA,OAAS,KAAA,EAAO;AACd,EAAA,OAAA,CAAQ,MAAA,CAAO,KAAA;AAAA,IACb,8CAA8C,KAAA,YAAiB,KAAA,GAAQ,MAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC;AAAA;AAAA;AAAA,GAEtG;AACA,EAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAChB","file":"db-url.js","sourcesContent":["/**\n * Database URL Derivation\n *\n * The config → connection-string translation. This was the retired CLI's job:\n * `gateway-start.ts` read the TOML it had already loaded and handed node a\n * `DATABASE_URL`. When the CLI went, so did the only thing that bridged the two\n * halves of the fact — the config knows the credentials, and the gateway only\n * ever read a connection string from the environment.\n *\n * Kept separate from the server entry point on purpose: the container derives\n * this in its CMD, BEFORE `prisma migrate deploy` and before node, because\n * migrate runs as its own process and would never see a value assembled inside\n * the server. See apps/gateway/Dockerfile and src/cli/db-url.ts.\n */\n\nimport type { EnvironmentConfig } from '@semiont/core';\n\n/**\n * Build a PostgreSQL connection string from `services.database`.\n *\n * Throws naming the missing key rather than returning a half-formed URL — a bad\n * connection string fails later and much less legibly than a missing one.\n */\nexport function databaseUrlFrom(config: EnvironmentConfig): string {\n const db = config.services?.database;\n if (!db) {\n throw new Error('services.database is required in environment config to derive DATABASE_URL');\n }\n\n // DatabaseServiceConfig accepts name/database and user/username as aliases.\n // The launcher writes name/user; hand-written configs use either.\n const name = db.name ?? db.database;\n const user = db.user ?? db.username;\n const missing = [\n ['host', db.host],\n ['port', db.port],\n ['name', name],\n ['user', user],\n ['password', db.password],\n ].filter(([, v]) => v === undefined || v === null || v === '').map(([k]) => k);\n\n if (missing.length > 0) {\n throw new Error(\n `services.database is missing ${missing.join(', ')} — needed to derive DATABASE_URL`,\n );\n }\n\n // new URL() rather than string interpolation: it percent-encodes the\n // credentials. The CLI built this with a template string, so a password\n // containing @ / : or ? produced a malformed URL that failed at connect time\n // with nothing pointing at the password as the cause.\n const url = new URL('postgresql://placeholder');\n url.username = String(user);\n url.password = String(db.password);\n url.hostname = String(db.host);\n url.port = String(db.port);\n url.pathname = `/${name}`;\n\n // Deliberately NO sslmode. The launcher's postgres:15.18-alpine serves no TLS,\n // so forcing sslmode=require would break every local stack; the CLI's string\n // carried none either. A deployment that needs TLS supplies DATABASE_URL\n // directly, which wins over this derivation.\n return url.toString();\n}\n","/**\n * Print the DATABASE_URL derived from this KB's config, and nothing else.\n *\n * (No shebang here — tsup's `banner` adds one to every entry; a second copy in\n * the source makes the bundle unparseable.)\n *\n * The container's CMD captures stdout and exports it before running\n * `prisma migrate deploy` and then the server:\n *\n * if [ -z \"$DATABASE_URL\" ]; then\n * DATABASE_URL=\"$(node \"$GATEWAY_DIR/dist/cli/db-url.js\")\"; export DATABASE_URL\n * fi\n *\n * Why a separate process rather than deriving it inside the server:\n *\n * 1. `migrate deploy` is its own process, started BEFORE the server. It reads\n * the datasource url from prisma.config.ts (`process.env.DATABASE_URL`), so\n * anything the server assembles internally is invisible to it — which is\n * why the documented DB_HOST/DB_USER/... component form never worked for\n * migrations.\n * 2. Even within the server it would be too late. The bundler emits module\n * bodies in dependency order, so src/db.ts's module-scope client\n * construction runs before index.ts's own top-level statements. Setting\n * DATABASE_URL before node starts sidesteps the ordering question entirely.\n *\n * And why the value is derived HERE rather than injected by the launcher: set\n * inside the container, the password never appears in `container inspect`. That\n * is the same rule the launcher already keeps for the admin password (see\n * gatewayArgs in apps/launcher/internal/launcher/start.go).\n *\n * Stdout is the contract: the URL, no trailing newline, nothing else. Diagnostics\n * go to stderr, and a failure exits non-zero so the CMD's `set -e` aborts before\n * migrating.\n */\n\nimport { loadEnvironmentConfig } from '@semiont/core/node';\nimport { databaseUrlFrom } from '../utils/database-url';\n\n// `null`, not SEMIONT_ROOT: the gateway mounts no piece of the knowledge base\n// (SINGLE-KB-MOUNT P6), so its image sets no SEMIONT_ROOT and there is no tree\n// here to point at. Its whole config input is the staged ~/.semiontconfig the\n// launcher bind-mounts, which is exactly what the loader reads when the project\n// root is null — the same call index.ts makes. Requiring the variable made this\n// step, and therefore every container start, fail before the server ever ran.\ntry {\n process.stdout.write(databaseUrlFrom(loadEnvironmentConfig(null)));\n} catch (error) {\n process.stderr.write(\n `Could not derive DATABASE_URL from config: ${error instanceof Error ? error.message : String(error)}\\n` +\n 'Set DATABASE_URL explicitly to bypass this derivation.\\n',\n );\n process.exit(1);\n}\n"]}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import * as argon2 from 'argon2';
|
|
4
|
+
import { PrismaClient } from '@prisma/client';
|
|
5
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
6
|
+
import { loadEnvironmentConfig } from '@semiont/core/node';
|
|
7
|
+
|
|
8
|
+
// src/utils/database-url.ts
|
|
9
|
+
function databaseUrlFrom(config) {
|
|
10
|
+
const db = config.services?.database;
|
|
11
|
+
if (!db) {
|
|
12
|
+
throw new Error("services.database is required in environment config to derive DATABASE_URL");
|
|
13
|
+
}
|
|
14
|
+
const name = db.name ?? db.database;
|
|
15
|
+
const user = db.user ?? db.username;
|
|
16
|
+
const missing = [
|
|
17
|
+
["host", db.host],
|
|
18
|
+
["port", db.port],
|
|
19
|
+
["name", name],
|
|
20
|
+
["user", user],
|
|
21
|
+
["password", db.password]
|
|
22
|
+
].filter(([, v]) => v === void 0 || v === null || v === "").map(([k]) => k);
|
|
23
|
+
if (missing.length > 0) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`services.database is missing ${missing.join(", ")} \u2014 needed to derive DATABASE_URL`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const url = new URL("postgresql://placeholder");
|
|
29
|
+
url.username = String(user);
|
|
30
|
+
url.password = String(db.password);
|
|
31
|
+
url.hostname = String(db.host);
|
|
32
|
+
url.port = String(db.port);
|
|
33
|
+
url.pathname = `/${name}`;
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/cli/useradd.ts
|
|
38
|
+
var USAGE = `Usage: semiont-useradd --email <email> [--password-stdin | --generate-password] [options]
|
|
39
|
+
|
|
40
|
+
Creating a user requires a password, so one of --password-stdin or
|
|
41
|
+
--generate-password. Updating an existing one does not: pass --password-stdin
|
|
42
|
+
only when the point is to CHANGE the password.
|
|
43
|
+
|
|
44
|
+
--email <email> User email address (required)
|
|
45
|
+
--password-stdin Read the password from stdin, first line (min 8 chars)
|
|
46
|
+
--generate-password Generate a random password (printed once)
|
|
47
|
+
--name <name> Display name
|
|
48
|
+
--admin Grant admin privileges
|
|
49
|
+
--moderator Grant moderator privileges
|
|
50
|
+
--inactive Create the user inactive
|
|
51
|
+
--update Update an existing user
|
|
52
|
+
--upsert Create if absent, succeed silently if present
|
|
53
|
+
--help, -h Show this help
|
|
54
|
+
`;
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const o = {
|
|
57
|
+
email: "",
|
|
58
|
+
passwordStdin: false,
|
|
59
|
+
generatePassword: false,
|
|
60
|
+
admin: false,
|
|
61
|
+
moderator: false,
|
|
62
|
+
inactive: false,
|
|
63
|
+
update: false,
|
|
64
|
+
upsert: false
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const a = argv[i];
|
|
68
|
+
const value = () => {
|
|
69
|
+
const v = argv[i + 1];
|
|
70
|
+
if (v === void 0 || v.startsWith("--")) throw new Error(`Missing value for ${a}`);
|
|
71
|
+
i++;
|
|
72
|
+
return v;
|
|
73
|
+
};
|
|
74
|
+
switch (a) {
|
|
75
|
+
case "--email":
|
|
76
|
+
o.email = value();
|
|
77
|
+
break;
|
|
78
|
+
case "--name":
|
|
79
|
+
o.name = value();
|
|
80
|
+
break;
|
|
81
|
+
case "--password-stdin":
|
|
82
|
+
o.passwordStdin = true;
|
|
83
|
+
break;
|
|
84
|
+
case "--generate-password":
|
|
85
|
+
o.generatePassword = true;
|
|
86
|
+
break;
|
|
87
|
+
case "--admin":
|
|
88
|
+
o.admin = true;
|
|
89
|
+
break;
|
|
90
|
+
case "--moderator":
|
|
91
|
+
o.moderator = true;
|
|
92
|
+
break;
|
|
93
|
+
case "--inactive":
|
|
94
|
+
o.inactive = true;
|
|
95
|
+
break;
|
|
96
|
+
case "--update":
|
|
97
|
+
o.update = true;
|
|
98
|
+
break;
|
|
99
|
+
case "--upsert":
|
|
100
|
+
o.upsert = true;
|
|
101
|
+
break;
|
|
102
|
+
case "--help":
|
|
103
|
+
case "-h":
|
|
104
|
+
process.stdout.write(USAGE);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unknown flag: ${a}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return o;
|
|
111
|
+
}
|
|
112
|
+
async function readPasswordFromStdin() {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
for await (const chunk of process.stdin) {
|
|
115
|
+
const buf = Buffer.from(chunk);
|
|
116
|
+
chunks.push(buf);
|
|
117
|
+
if (buf.includes(10)) break;
|
|
118
|
+
}
|
|
119
|
+
const first = Buffer.concat(chunks).toString("utf8").split("\n", 1)[0] ?? "";
|
|
120
|
+
const password = first.replace(/\r$/, "");
|
|
121
|
+
if (!password) throw new Error("--password-stdin was given but stdin carried no password");
|
|
122
|
+
if (password.length < 8) throw new Error("Password must be at least 8 characters long");
|
|
123
|
+
return password;
|
|
124
|
+
}
|
|
125
|
+
function generatePassword() {
|
|
126
|
+
return crypto.randomBytes(12).toString("base64");
|
|
127
|
+
}
|
|
128
|
+
function domainOf(email) {
|
|
129
|
+
const parts = email.split("@");
|
|
130
|
+
if (parts.length !== 2 || !parts[1]) {
|
|
131
|
+
throw new Error(`Cannot extract domain from email: ${email}`);
|
|
132
|
+
}
|
|
133
|
+
return parts[1];
|
|
134
|
+
}
|
|
135
|
+
function validate(o) {
|
|
136
|
+
if (!o.email) throw new Error("--email is required");
|
|
137
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(o.email)) {
|
|
138
|
+
throw new Error(`invalid email format: ${o.email}`);
|
|
139
|
+
}
|
|
140
|
+
if (o.passwordStdin && o.generatePassword) {
|
|
141
|
+
throw new Error("--password-stdin and --generate-password are mutually exclusive");
|
|
142
|
+
}
|
|
143
|
+
if (o.update && o.upsert) {
|
|
144
|
+
throw new Error("--update and --upsert are mutually exclusive");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function main(argv) {
|
|
148
|
+
const o = parseArgs(argv);
|
|
149
|
+
validate(o);
|
|
150
|
+
const config = loadEnvironmentConfig(null);
|
|
151
|
+
const connectionString = process.env.DATABASE_URL || databaseUrlFrom(config);
|
|
152
|
+
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) });
|
|
153
|
+
try {
|
|
154
|
+
const existing = await prisma.user.findUnique({ where: { email: o.email } });
|
|
155
|
+
let passwordHash;
|
|
156
|
+
if (o.generatePassword) {
|
|
157
|
+
const generated = generatePassword();
|
|
158
|
+
passwordHash = await argon2.hash(generated);
|
|
159
|
+
process.stdout.write(`Generated password: ${generated}
|
|
160
|
+
`);
|
|
161
|
+
} else if (o.passwordStdin) {
|
|
162
|
+
passwordHash = await argon2.hash(await readPasswordFromStdin());
|
|
163
|
+
} else if (!existing) {
|
|
164
|
+
throw new Error("Password required: use --password-stdin or --generate-password");
|
|
165
|
+
}
|
|
166
|
+
if (existing) {
|
|
167
|
+
if (o.upsert) {
|
|
168
|
+
process.stdout.write(`User already exists: ${o.email}
|
|
169
|
+
`);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
if (!o.update) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`User ${o.email} already exists. Use --update to modify or --upsert to skip silently.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
await prisma.user.update({
|
|
178
|
+
where: { email: o.email },
|
|
179
|
+
data: {
|
|
180
|
+
...passwordHash ? { passwordHash } : {},
|
|
181
|
+
...o.name !== void 0 ? { name: o.name } : {},
|
|
182
|
+
...o.admin ? { isAdmin: true } : {},
|
|
183
|
+
...o.moderator ? { isModerator: true } : {},
|
|
184
|
+
...o.inactive ? { isActive: false } : {}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
process.stdout.write(`User updated: ${o.email}
|
|
188
|
+
`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (o.update) {
|
|
192
|
+
throw new Error(`User ${o.email} not found. Remove --update to create a new user.`);
|
|
193
|
+
}
|
|
194
|
+
await prisma.user.create({
|
|
195
|
+
data: {
|
|
196
|
+
email: o.email,
|
|
197
|
+
name: o.name ?? null,
|
|
198
|
+
// provider/providerId must match what POST /api/tokens/password looks
|
|
199
|
+
// for: it rejects any account whose provider is not 'password'.
|
|
200
|
+
provider: "password",
|
|
201
|
+
providerId: o.email,
|
|
202
|
+
passwordHash,
|
|
203
|
+
domain: domainOf(o.email),
|
|
204
|
+
isActive: !o.inactive,
|
|
205
|
+
isAdmin: o.admin,
|
|
206
|
+
isModerator: o.moderator
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
process.stdout.write(`User created: ${o.email}
|
|
210
|
+
`);
|
|
211
|
+
if (o.admin) process.stdout.write(" Role: Admin\n");
|
|
212
|
+
if (o.moderator) process.stdout.write(" Role: Moderator\n");
|
|
213
|
+
return 0;
|
|
214
|
+
} finally {
|
|
215
|
+
await prisma.$disconnect();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main(process.argv.slice(2)).then((code) => process.exit(code)).catch((error) => {
|
|
219
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
220
|
+
`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
|
223
|
+
//# sourceMappingURL=useradd.js.map
|
|
224
|
+
//# sourceMappingURL=useradd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/database-url.ts","../../src/cli/useradd.ts"],"names":[],"mappings":"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","file":"useradd.js","sourcesContent":["/**\n * Database URL Derivation\n *\n * The config → connection-string translation. This was the retired CLI's job:\n * `gateway-start.ts` read the TOML it had already loaded and handed node a\n * `DATABASE_URL`. When the CLI went, so did the only thing that bridged the two\n * halves of the fact — the config knows the credentials, and the gateway only\n * ever read a connection string from the environment.\n *\n * Kept separate from the server entry point on purpose: the container derives\n * this in its CMD, BEFORE `prisma migrate deploy` and before node, because\n * migrate runs as its own process and would never see a value assembled inside\n * the server. See apps/gateway/Dockerfile and src/cli/db-url.ts.\n */\n\nimport type { EnvironmentConfig } from '@semiont/core';\n\n/**\n * Build a PostgreSQL connection string from `services.database`.\n *\n * Throws naming the missing key rather than returning a half-formed URL — a bad\n * connection string fails later and much less legibly than a missing one.\n */\nexport function databaseUrlFrom(config: EnvironmentConfig): string {\n const db = config.services?.database;\n if (!db) {\n throw new Error('services.database is required in environment config to derive DATABASE_URL');\n }\n\n // DatabaseServiceConfig accepts name/database and user/username as aliases.\n // The launcher writes name/user; hand-written configs use either.\n const name = db.name ?? db.database;\n const user = db.user ?? db.username;\n const missing = [\n ['host', db.host],\n ['port', db.port],\n ['name', name],\n ['user', user],\n ['password', db.password],\n ].filter(([, v]) => v === undefined || v === null || v === '').map(([k]) => k);\n\n if (missing.length > 0) {\n throw new Error(\n `services.database is missing ${missing.join(', ')} — needed to derive DATABASE_URL`,\n );\n }\n\n // new URL() rather than string interpolation: it percent-encodes the\n // credentials. The CLI built this with a template string, so a password\n // containing @ / : or ? produced a malformed URL that failed at connect time\n // with nothing pointing at the password as the cause.\n const url = new URL('postgresql://placeholder');\n url.username = String(user);\n url.password = String(db.password);\n url.hostname = String(db.host);\n url.port = String(db.port);\n url.pathname = `/${name}`;\n\n // Deliberately NO sslmode. The launcher's postgres:15.18-alpine serves no TLS,\n // so forcing sslmode=require would break every local stack; the CLI's string\n // carried none either. A deployment that needs TLS supplies DATABASE_URL\n // directly, which wins over this derivation.\n return url.toString();\n}\n","/**\n * Create or update a user. Invoked inside the gateway container by\n * `semiont useradd`, which execs it and passes every flag through verbatim.\n *\n * (No shebang — tsup's `banner` adds one to every entry.)\n *\n * Why this lives in the gateway rather than the launcher: creating a user is a\n * schema-shaped operation. Two columns carry NO database-side default —\n *\n * \"id\" TEXT NOT NULL -- @default(cuid()), applied client-side\n * \"updatedAt\" TIMESTAMP(3) NOT NULL -- @updatedAt, applied client-side\n *\n * — so a writer outside Prisma has to generate a cuid, supply updatedAt, know the\n * physical column names, and match argon2's PHC parameters. Each is doable; the\n * durable cost is that a future migration adding a NOT NULL column breaks such a\n * writer SILENTLY, discovered the next time someone creates a user. Here, the\n * generated client changes with the schema and `tsc` fails in CI.\n *\n * It also keeps the launcher technology-agnostic: it runs containers and decides\n * which stack is meant. It does not need to know that postgres, argon2, or cuids\n * exist.\n *\n * DATABASE_URL is derived here, not inherited: `container exec` starts a process\n * from the IMAGE's env, so nothing the CMD exported is visible to it (verified).\n * That is why databaseUrlFrom is a standalone helper.\n */\n\nimport * as crypto from 'crypto';\nimport * as argon2 from 'argon2';\nimport { PrismaClient } from '@prisma/client';\nimport { PrismaPg } from '@prisma/adapter-pg';\nimport { loadEnvironmentConfig } from '@semiont/core/node';\nimport { databaseUrlFrom } from '../utils/database-url';\n\ninterface Options {\n email: string;\n passwordStdin: boolean;\n generatePassword: boolean;\n name?: string;\n admin: boolean;\n moderator: boolean;\n inactive: boolean;\n update: boolean;\n upsert: boolean;\n}\n\nconst USAGE = `Usage: semiont-useradd --email <email> [--password-stdin | --generate-password] [options]\n\nCreating a user requires a password, so one of --password-stdin or\n--generate-password. Updating an existing one does not: pass --password-stdin\nonly when the point is to CHANGE the password.\n\n --email <email> User email address (required)\n --password-stdin Read the password from stdin, first line (min 8 chars)\n --generate-password Generate a random password (printed once)\n --name <name> Display name\n --admin Grant admin privileges\n --moderator Grant moderator privileges\n --inactive Create the user inactive\n --update Update an existing user\n --upsert Create if absent, succeed silently if present\n --help, -h Show this help\n`;\n\nfunction parseArgs(argv: string[]): Options {\n const o: Options = {\n email: '', passwordStdin: false, generatePassword: false, admin: false,\n moderator: false, inactive: false, update: false, upsert: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n const value = (): string => {\n const v = argv[i + 1];\n if (v === undefined || v.startsWith('--')) throw new Error(`Missing value for ${a}`);\n i++;\n return v;\n };\n switch (a) {\n case '--email': o.email = value(); break;\n case '--name': o.name = value(); break;\n case '--password-stdin': o.passwordStdin = true; break;\n case '--generate-password': o.generatePassword = true; break;\n case '--admin': o.admin = true; break;\n case '--moderator': o.moderator = true; break;\n case '--inactive': o.inactive = true; break;\n case '--update': o.update = true; break;\n case '--upsert': o.upsert = true; break;\n case '--help': case '-h': process.stdout.write(USAGE); process.exit(0);\n default: throw new Error(`Unknown flag: ${a}`);\n }\n }\n return o;\n}\n\n/**\n * Read the password from stdin's FIRST LINE.\n *\n * A password must never travel in argv: `ps` shows a process's command line to\n * every other user on the host, `docker inspect`/`container inspect` keep it as\n * long as the container record lives, and the caller's shell records it in\n * history. Stdin has none of those properties.\n */\nasync function readPasswordFromStdin(): Promise<string> {\n // Stop at the first newline rather than draining to EOF: a password is one\n // line, and waiting for the stream to close would hang an interactive run\n // (`docker exec -it … --password-stdin`) after the user pressed Enter, until\n // they thought to send Ctrl-D.\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n const buf = Buffer.from(chunk);\n chunks.push(buf);\n if (buf.includes(0x0a)) break;\n }\n // split() on empty input yields [''], but noUncheckedIndexedAccess types the\n // index access as possibly-undefined regardless — empty stdin is a refusal\n // either way, two lines down.\n const first = Buffer.concat(chunks).toString('utf8').split('\\n', 1)[0] ?? '';\n const password = first.replace(/\\r$/, '');\n if (!password) throw new Error('--password-stdin was given but stdin carried no password');\n if (password.length < 8) throw new Error('Password must be at least 8 characters long');\n return password;\n}\n\n/** Same shape the old CLI produced: 16 base64 chars from 12 random bytes. */\nfunction generatePassword(): string {\n return crypto.randomBytes(12).toString('base64');\n}\n\nfunction domainOf(email: string): string {\n const parts = email.split('@');\n if (parts.length !== 2 || !parts[1]) {\n throw new Error(`Cannot extract domain from email: ${email}`);\n }\n return parts[1];\n}\n\nfunction validate(o: Options): void {\n if (!o.email) throw new Error('--email is required');\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(o.email)) {\n throw new Error(`invalid email format: ${o.email}`);\n }\n if (o.passwordStdin && o.generatePassword) {\n throw new Error('--password-stdin and --generate-password are mutually exclusive');\n }\n if (o.update && o.upsert) {\n throw new Error('--update and --upsert are mutually exclusive');\n }\n}\n\nasync function main(argv: string[]): Promise<number> {\n const o = parseArgs(argv);\n validate(o);\n\n // `null` for the same reason db-url.ts passes null: this bin is exec'd INSIDE\n // the gateway container (`container exec semiont-gateway semiont-useradd`),\n // which mounts no knowledge base and sets no SEMIONT_ROOT. The staged\n // ~/.semiontconfig is the config, and the loader reads it either way.\n const config = loadEnvironmentConfig(null);\n\n // An explicit DATABASE_URL still wins, matching the container CMD's precedence.\n const connectionString = process.env.DATABASE_URL || databaseUrlFrom(config);\n const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) });\n\n try {\n const existing = await prisma.user.findUnique({ where: { email: o.email } });\n\n let passwordHash: string | undefined;\n if (o.generatePassword) {\n const generated = generatePassword();\n passwordHash = await argon2.hash(generated);\n // Printed once and never stored: the caller's only chance to capture it.\n process.stdout.write(`Generated password: ${generated}\\n`);\n } else if (o.passwordStdin) {\n passwordHash = await argon2.hash(await readPasswordFromStdin());\n } else if (!existing) {\n throw new Error('Password required: use --password-stdin or --generate-password');\n }\n\n if (existing) {\n if (o.upsert) {\n process.stdout.write(`User already exists: ${o.email}\\n`);\n return 0;\n }\n if (!o.update) {\n throw new Error(\n `User ${o.email} already exists. Use --update to modify or --upsert to skip silently.`,\n );\n }\n await prisma.user.update({\n where: { email: o.email },\n data: {\n ...(passwordHash ? { passwordHash } : {}),\n ...(o.name !== undefined ? { name: o.name } : {}),\n ...(o.admin ? { isAdmin: true } : {}),\n ...(o.moderator ? { isModerator: true } : {}),\n ...(o.inactive ? { isActive: false } : {}),\n },\n });\n process.stdout.write(`User updated: ${o.email}\\n`);\n return 0;\n }\n\n if (o.update) {\n throw new Error(`User ${o.email} not found. Remove --update to create a new user.`);\n }\n\n await prisma.user.create({\n data: {\n email: o.email,\n name: o.name ?? null,\n // provider/providerId must match what POST /api/tokens/password looks\n // for: it rejects any account whose provider is not 'password'.\n provider: 'password',\n providerId: o.email,\n passwordHash: passwordHash!,\n domain: domainOf(o.email),\n isActive: !o.inactive,\n isAdmin: o.admin,\n isModerator: o.moderator,\n },\n });\n process.stdout.write(`User created: ${o.email}\\n`);\n if (o.admin) process.stdout.write(' Role: Admin\\n');\n if (o.moderator) process.stdout.write(' Role: Moderator\\n');\n return 0;\n } finally {\n await prisma.$disconnect();\n }\n}\n\nmain(process.argv.slice(2))\n .then((code) => process.exit(code))\n .catch((error) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exit(1);\n });\n"]}
|