@promptowl/contextnest-community 1.9.0 → 1.10.0
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/CONFIGURATION.md +29 -2
- package/dist/{chunk-TIO5XOFD.js → chunk-7PQREKSM.js} +1 -1
- package/dist/{chunk-DPHV6Q26.js → chunk-BC6KFUZH.js} +205 -22
- package/dist/{chunk-BYS4HDME.js → chunk-KFGZIECB.js} +73 -26
- package/dist/{chunk-HRQWNRVI.js → chunk-UHUU3VAK.js} +1 -1
- package/dist/{chunk-VC3QNHHB.js → chunk-ZM4F7MY7.js} +685 -6
- package/dist/{client-GI74NPIW.js → client-VZLX4THW.js} +1 -1
- package/dist/{grants-service-MDLRPMZR.js → grants-service-6H4WUA2F.js} +2 -2
- package/dist/index.js +1953 -521
- package/dist/{migrations.postgres-ORSV7UFJ.js → migrations.postgres-4XYY3CTF.js} +100 -2
- package/dist/{review-service-A3XFQTJH.js → review-service-VLSUT6KK.js} +5 -4
- package/dist/{stewardship-service-3KCVVXCW.js → stewardship-service-D5PDWXBR.js} +2 -2
- package/dist/{version-service-6J4OWQL6.js → version-service-A2YAKYQW.js} +2 -2
- package/dist/web3/assets/index-B2PkvzmY.js +1007 -0
- package/dist/web3/assets/index-C-tX933G.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -2
- package/dist/web3/assets/index-CHavHPIT.css +0 -1
- package/dist/web3/assets/index-DyvOBH92.js +0 -934
package/CONFIGURATION.md
CHANGED
|
@@ -56,13 +56,15 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
56
56
|
| `DB_SSL_CA` | `""` | Path to a CA certificate (PEM) for verify-ca/verify-full TLS when `DB_SSL=true`. |
|
|
57
57
|
| `AUTH_MODE` | `key` | `key` or `open`. See above. |
|
|
58
58
|
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
59
|
-
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to `
|
|
59
|
+
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
60
60
|
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
61
|
+
| `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
|
|
61
62
|
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Official deployment only — leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
|
|
62
63
|
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another. Only relevant when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. |
|
|
63
|
-
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to
|
|
64
|
+
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
|
|
64
65
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
65
66
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
67
|
+
| `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
|
|
66
68
|
| `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
|
|
67
69
|
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
|
|
68
70
|
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
|
|
@@ -103,6 +105,31 @@ Postgres backend starts empty.
|
|
|
103
105
|
> shared storage — e.g. mount a GCS bucket via Cloud Storage FUSE, or use a
|
|
104
106
|
> persistent volume — otherwise documents will disappear on redeploy/scale-in.
|
|
105
107
|
|
|
108
|
+
### Runtime settings persistence
|
|
109
|
+
|
|
110
|
+
Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
|
|
111
|
+
sign-in gate, logo, base URL, upload limit, feature flags, Slack/SMTP connectors)
|
|
112
|
+
plus the **installed license key** — are stored in the database (`server_settings`
|
|
113
|
+
table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
|
|
114
|
+
filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
|
|
115
|
+
horizontally-scaled instance had its own filesystem, so a change on one never
|
|
116
|
+
reached the others. The database (Cloud SQL in production) is durable **and** shared,
|
|
117
|
+
so a UI change persists across rebuilds and is seen by every instance. Rows are
|
|
118
|
+
loaded into the process environment at boot.
|
|
119
|
+
|
|
120
|
+
**Precedence between the deploy environment and a UI change.** Each stored row
|
|
121
|
+
remembers the deploy-env value in effect when it was written. At boot:
|
|
122
|
+
|
|
123
|
+
- If the deploy env value for that key is **unchanged**, the stored UI change wins
|
|
124
|
+
(your Settings-page edit survives an identical rebuild).
|
|
125
|
+
- If the deploy env value **changed** (you redeployed with a new value for that
|
|
126
|
+
variable), the deploy env wins and the stored row is re-synced to it.
|
|
127
|
+
|
|
128
|
+
So you can always override any setting from the deploy config, while UI edits made
|
|
129
|
+
to keys you don't set in the environment stay put. Clearing a setting in the UI
|
|
130
|
+
writes a tombstone, so a value you removed is not resurrected from the environment
|
|
131
|
+
on the next boot.
|
|
132
|
+
|
|
106
133
|
### Cloud Run + Cloud SQL (PostgreSQL)
|
|
107
134
|
|
|
108
135
|
Attach the Cloud SQL instance to the service (`--add-cloudsql-instances`) so the
|
|
@@ -29,7 +29,7 @@ var slackUrlWarned = false;
|
|
|
29
29
|
var emailFromWarned = false;
|
|
30
30
|
var emailToWarned = false;
|
|
31
31
|
function isEmailish(v) {
|
|
32
|
-
return /^[^\s@]+@[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
|
|
32
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
|
|
33
33
|
}
|
|
34
34
|
function isEmailListish(v) {
|
|
35
35
|
return v.split(",").every((e) => isEmailish(e.trim()));
|
|
@@ -124,6 +124,18 @@ var config = {
|
|
|
124
124
|
get PROMPTOWL_KEY() {
|
|
125
125
|
return process.env.PROMPTOWL_KEY || "";
|
|
126
126
|
},
|
|
127
|
+
/** Vercel automation-bypass secret for PO calls; unset = no bypass. */
|
|
128
|
+
get PROMPTOWL_BYPASS_SECRET() {
|
|
129
|
+
return process.env.PROMPTOWL_BYPASS_SECRET || "";
|
|
130
|
+
},
|
|
131
|
+
/** Outbound headers for PO fetches; bypass header only when secret set. */
|
|
132
|
+
get PROMPTOWL_FETCH_HEADERS() {
|
|
133
|
+
const h = { "Content-Type": "application/json" };
|
|
134
|
+
if (this.PROMPTOWL_BYPASS_SECRET) {
|
|
135
|
+
h["x-vercel-protection-bypass"] = this.PROMPTOWL_BYPASS_SECRET;
|
|
136
|
+
}
|
|
137
|
+
return h;
|
|
138
|
+
},
|
|
127
139
|
/**
|
|
128
140
|
* Shared secret for one-click SSO auto-login from PromptOwl (PO).
|
|
129
141
|
*
|
|
@@ -164,6 +176,24 @@ var config = {
|
|
|
164
176
|
const v = (process.env.PROMPTOWL_SIGN_IN_GATE || "open").trim().toLowerCase();
|
|
165
177
|
return v === "admin-only" || v === "disabled" ? v : "open";
|
|
166
178
|
},
|
|
179
|
+
/**
|
|
180
|
+
* Manual (email + password) sign-in. Three modes, independent of
|
|
181
|
+
* `PROMPTOWL_SIGN_IN_GATE` (the two methods are controlled separately):
|
|
182
|
+
* "open" — anyone may log in and self-register (default).
|
|
183
|
+
* "invite-only" — existing + admin-invited users may log in and an invited
|
|
184
|
+
* placeholder may set its password ("claim"), but brand-new
|
|
185
|
+
* self-registration is refused. Use this when the admin
|
|
186
|
+
* provisions accounts (invite / share / steward) and users
|
|
187
|
+
* shouldn't be able to create their own.
|
|
188
|
+
* "disabled" — no email/password sign-in at all (PromptOwl only).
|
|
189
|
+
* Enforced at POST /auth/login and POST /auth/register. The /admin/settings
|
|
190
|
+
* validator refuses "disabled" while PromptOwl is also disabled, so a server
|
|
191
|
+
* can never be left with no way to sign in.
|
|
192
|
+
*/
|
|
193
|
+
get MANUAL_SIGN_IN() {
|
|
194
|
+
const v = (process.env.MANUAL_SIGN_IN || "open").trim().toLowerCase();
|
|
195
|
+
return v === "invite-only" || v === "disabled" ? v : "open";
|
|
196
|
+
},
|
|
167
197
|
/**
|
|
168
198
|
* Path to the .env file the server reads its config from and the license
|
|
169
199
|
* install flow persists PROMPTOWL_KEY into. Defaults UNDER DATA_ROOT (not
|
|
@@ -181,6 +211,18 @@ var config = {
|
|
|
181
211
|
get TELEMETRY_INTERVAL_MS() {
|
|
182
212
|
return parseInt(process.env.TELEMETRY_INTERVAL_MS || "3600000", 10);
|
|
183
213
|
},
|
|
214
|
+
/**
|
|
215
|
+
* Activity-trace retention window in days (the api_events table behind
|
|
216
|
+
* GET /admin/trace and GET /nests/:id/trace). 0 = keep forever (pruning is
|
|
217
|
+
* skipped entirely). Invalid / negative → default 14; capped at 3650 (ten
|
|
218
|
+
* years) so a typo can't schedule a prune cutoff in the distant past.
|
|
219
|
+
* Editable at runtime from the admin Settings page (/admin/settings).
|
|
220
|
+
*/
|
|
221
|
+
get TRACE_RETENTION_DAYS() {
|
|
222
|
+
const n = parseInt(process.env.TRACE_RETENTION_DAYS || "14", 10);
|
|
223
|
+
if (!Number.isFinite(n) || n < 0) return 14;
|
|
224
|
+
return Math.min(n, 3650);
|
|
225
|
+
},
|
|
184
226
|
/**
|
|
185
227
|
* Optional custom logo URL shown in UI header + login screen.
|
|
186
228
|
* Must be an absolute https://, http://, or data:image/… URL. Other
|
|
@@ -206,6 +248,26 @@ var config = {
|
|
|
206
248
|
* This flag is also the future license-tier hook: gating a paid tier here
|
|
207
249
|
* is a one-line change because every plane route checks it per request.
|
|
208
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* Optional Slack incoming-webhook URL for team notifications (review
|
|
253
|
+
* submitted/approved/rejected). Empty/unset = connector off. https only —
|
|
254
|
+
* a webhook carries an implicit secret in its path, so it never travels
|
|
255
|
+
* plaintext. (Ported from development PR #105.)
|
|
256
|
+
*/
|
|
257
|
+
get SLACK_WEBHOOK_URL() {
|
|
258
|
+
const raw = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
259
|
+
if (!raw) return null;
|
|
260
|
+
if (!/^https:\/\//i.test(raw)) {
|
|
261
|
+
if (!slackUrlWarned) {
|
|
262
|
+
slackUrlWarned = true;
|
|
263
|
+
console.warn(
|
|
264
|
+
"[config] SLACK_WEBHOOK_URL rejected: must be an https:// URL. Slack notifications disabled."
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return raw;
|
|
270
|
+
},
|
|
209
271
|
get FEATURE_WORKFLOW_PLANE() {
|
|
210
272
|
return process.env.FEATURE_WORKFLOW_PLANE === "true";
|
|
211
273
|
},
|
|
@@ -238,6 +300,13 @@ var config = {
|
|
|
238
300
|
const n = parseInt(process.env.RUN_MAX_STEPS || "10000", 10);
|
|
239
301
|
return Number.isFinite(n) ? Math.min(1e5, Math.max(10, n)) : 1e4;
|
|
240
302
|
},
|
|
303
|
+
/** Max concurrently-RUNNING root (depth-0) runs per nest. The subagent
|
|
304
|
+
* depth/fan-out caps bound a single tree; this bounds how many trees a
|
|
305
|
+
* caller can start at once, so root triggers can't flood the nest. */
|
|
306
|
+
get RUN_MAX_CONCURRENT_ROOTS() {
|
|
307
|
+
const n = parseInt(process.env.RUN_MAX_CONCURRENT_ROOTS || "50", 10);
|
|
308
|
+
return Number.isFinite(n) ? Math.min(1e3, Math.max(1, n)) : 50;
|
|
309
|
+
},
|
|
241
310
|
get AUTH_MODE() {
|
|
242
311
|
return process.env.AUTH_MODE || "key";
|
|
243
312
|
},
|
|
@@ -295,26 +364,6 @@ var config = {
|
|
|
295
364
|
}
|
|
296
365
|
return raw;
|
|
297
366
|
},
|
|
298
|
-
/**
|
|
299
|
-
* Optional Slack incoming-webhook URL for team notifications (review
|
|
300
|
-
* submitted/approved/rejected, nest shared). Empty/unset = connector off.
|
|
301
|
-
* https only — a webhook carries an implicit secret in its path, so it
|
|
302
|
-
* never travels plaintext.
|
|
303
|
-
*/
|
|
304
|
-
get SLACK_WEBHOOK_URL() {
|
|
305
|
-
const raw = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
306
|
-
if (!raw) return null;
|
|
307
|
-
if (!/^https:\/\//i.test(raw)) {
|
|
308
|
-
if (!slackUrlWarned) {
|
|
309
|
-
slackUrlWarned = true;
|
|
310
|
-
console.warn(
|
|
311
|
-
"[config] SLACK_WEBHOOK_URL rejected: must be an https:// URL. Slack notifications disabled."
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
return raw;
|
|
317
|
-
},
|
|
318
367
|
/**
|
|
319
368
|
* Per-nest notification digest window in milliseconds. A burst of governance
|
|
320
369
|
* events on one nest inside this window collapses into a single digest
|
|
@@ -1078,6 +1127,140 @@ function runMigrations(db) {
|
|
|
1078
1127
|
})();
|
|
1079
1128
|
recordMigration("018_subagent_runs");
|
|
1080
1129
|
}
|
|
1130
|
+
if (!hasMigration("020_server_settings")) {
|
|
1131
|
+
db.transaction(() => {
|
|
1132
|
+
db.exec(`
|
|
1133
|
+
CREATE TABLE IF NOT EXISTS server_settings (
|
|
1134
|
+
key TEXT PRIMARY KEY,
|
|
1135
|
+
value TEXT,
|
|
1136
|
+
env_at_write TEXT,
|
|
1137
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1138
|
+
);
|
|
1139
|
+
`);
|
|
1140
|
+
recordMigration("020_server_settings");
|
|
1141
|
+
})();
|
|
1142
|
+
}
|
|
1143
|
+
if (!hasMigration("020_schedules")) {
|
|
1144
|
+
db.transaction(() => {
|
|
1145
|
+
db.exec(`
|
|
1146
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
1147
|
+
id TEXT PRIMARY KEY,
|
|
1148
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1149
|
+
agent_node TEXT NOT NULL,
|
|
1150
|
+
every_minutes INTEGER NOT NULL CHECK(every_minutes >= 5),
|
|
1151
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1152
|
+
last_run_at TEXT,
|
|
1153
|
+
created_by TEXT NOT NULL,
|
|
1154
|
+
created_at TEXT NOT NULL,
|
|
1155
|
+
updated_at TEXT NOT NULL
|
|
1156
|
+
);
|
|
1157
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_agent
|
|
1158
|
+
ON schedules(nest_id, agent_node);
|
|
1159
|
+
`);
|
|
1160
|
+
})();
|
|
1161
|
+
recordMigration("020_schedules");
|
|
1162
|
+
}
|
|
1163
|
+
if (!hasMigration("021_schedule_bounds")) {
|
|
1164
|
+
db.transaction(() => {
|
|
1165
|
+
db.exec(`
|
|
1166
|
+
ALTER TABLE schedules ADD COLUMN max_runs INTEGER;
|
|
1167
|
+
ALTER TABLE schedules ADD COLUMN runs_created INTEGER NOT NULL DEFAULT 0;
|
|
1168
|
+
`);
|
|
1169
|
+
})();
|
|
1170
|
+
recordMigration("021_schedule_bounds");
|
|
1171
|
+
}
|
|
1172
|
+
if (!hasMigration("022_tools_and_watchers")) {
|
|
1173
|
+
db.transaction(() => {
|
|
1174
|
+
db.exec(`
|
|
1175
|
+
CREATE TABLE IF NOT EXISTS nest_env (
|
|
1176
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1177
|
+
key TEXT NOT NULL,
|
|
1178
|
+
value TEXT NOT NULL,
|
|
1179
|
+
updated_by TEXT NOT NULL,
|
|
1180
|
+
updated_at TEXT NOT NULL,
|
|
1181
|
+
PRIMARY KEY (nest_id, key)
|
|
1182
|
+
);
|
|
1183
|
+
CREATE TABLE IF NOT EXISTS watchers (
|
|
1184
|
+
id TEXT PRIMARY KEY,
|
|
1185
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1186
|
+
node_id TEXT NOT NULL,
|
|
1187
|
+
user_email TEXT NOT NULL,
|
|
1188
|
+
created_by TEXT NOT NULL,
|
|
1189
|
+
created_at TEXT NOT NULL
|
|
1190
|
+
);
|
|
1191
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_watchers_unique
|
|
1192
|
+
ON watchers(nest_id, node_id, user_email);
|
|
1193
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
1194
|
+
id TEXT PRIMARY KEY,
|
|
1195
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1196
|
+
user_email TEXT NOT NULL,
|
|
1197
|
+
kind TEXT NOT NULL,
|
|
1198
|
+
subject_id TEXT,
|
|
1199
|
+
message TEXT NOT NULL,
|
|
1200
|
+
created_at TEXT NOT NULL,
|
|
1201
|
+
read_at TEXT
|
|
1202
|
+
);
|
|
1203
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user
|
|
1204
|
+
ON notifications(user_email, read_at, created_at);
|
|
1205
|
+
`);
|
|
1206
|
+
})();
|
|
1207
|
+
recordMigration("022_tools_and_watchers");
|
|
1208
|
+
}
|
|
1209
|
+
if (!hasMigration("023_connectors")) {
|
|
1210
|
+
db.transaction(() => {
|
|
1211
|
+
db.exec(`
|
|
1212
|
+
CREATE TABLE IF NOT EXISTS connectors (
|
|
1213
|
+
id TEXT PRIMARY KEY,
|
|
1214
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1215
|
+
channel TEXT NOT NULL CHECK(channel IN ('slack', 'teams', 'webhook')),
|
|
1216
|
+
url TEXT NOT NULL, -- https://\u2026 or env:KEY
|
|
1217
|
+
events TEXT NOT NULL, -- JSON array of kinds, or ["*"]
|
|
1218
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1219
|
+
created_by TEXT NOT NULL,
|
|
1220
|
+
created_at TEXT NOT NULL,
|
|
1221
|
+
updated_at TEXT NOT NULL
|
|
1222
|
+
);
|
|
1223
|
+
CREATE INDEX IF NOT EXISTS idx_connectors_nest ON connectors(nest_id);
|
|
1224
|
+
`);
|
|
1225
|
+
})();
|
|
1226
|
+
recordMigration("023_connectors");
|
|
1227
|
+
}
|
|
1228
|
+
if (!hasMigration("024_trigger_hooks")) {
|
|
1229
|
+
db.transaction(() => {
|
|
1230
|
+
db.exec(`
|
|
1231
|
+
CREATE TABLE IF NOT EXISTS trigger_hooks (
|
|
1232
|
+
id TEXT PRIMARY KEY, -- the token (hk_\u2026), capability auth
|
|
1233
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1234
|
+
agent_node TEXT NOT NULL,
|
|
1235
|
+
preset TEXT NOT NULL CHECK(preset IN ('slack', 'teams', 'webhook')),
|
|
1236
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1237
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
1238
|
+
last_fired_at TEXT,
|
|
1239
|
+
created_by TEXT NOT NULL,
|
|
1240
|
+
created_at TEXT NOT NULL
|
|
1241
|
+
);
|
|
1242
|
+
CREATE INDEX IF NOT EXISTS idx_hooks_nest ON trigger_hooks(nest_id);
|
|
1243
|
+
`);
|
|
1244
|
+
})();
|
|
1245
|
+
recordMigration("024_trigger_hooks");
|
|
1246
|
+
}
|
|
1247
|
+
if (!hasMigration("025_run_claims")) {
|
|
1248
|
+
db.transaction(() => {
|
|
1249
|
+
db.exec(`
|
|
1250
|
+
ALTER TABLE runs ADD COLUMN claimed_by TEXT;
|
|
1251
|
+
ALTER TABLE runs ADD COLUMN claimed_at TEXT;
|
|
1252
|
+
`);
|
|
1253
|
+
})();
|
|
1254
|
+
recordMigration("025_run_claims");
|
|
1255
|
+
}
|
|
1256
|
+
if (!hasMigration("026_api_events_nest_index")) {
|
|
1257
|
+
db.transaction(() => {
|
|
1258
|
+
db.exec(`
|
|
1259
|
+
CREATE INDEX IF NOT EXISTS idx_api_events_nest_ts ON api_events(nest_id, id);
|
|
1260
|
+
`);
|
|
1261
|
+
})();
|
|
1262
|
+
recordMigration("026_api_events_nest_index");
|
|
1263
|
+
}
|
|
1081
1264
|
}
|
|
1082
1265
|
function mergeCaseCollidingUsers(db) {
|
|
1083
1266
|
const groups = db.prepare(
|
|
@@ -1295,7 +1478,7 @@ async function initDb() {
|
|
|
1295
1478
|
if (config.DB_DRIVER === "postgres") {
|
|
1296
1479
|
const { Pool } = await import("pg");
|
|
1297
1480
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1298
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1481
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-4XYY3CTF.js");
|
|
1299
1482
|
const pool = new Pool(buildPgConfig());
|
|
1300
1483
|
adapter = new PostgresAdapter(pool);
|
|
1301
1484
|
await runPostgresMigrations(adapter);
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
config,
|
|
11
11
|
getDb
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-BC6KFUZH.js";
|
|
13
13
|
import {
|
|
14
14
|
ANON_USER_ID
|
|
15
15
|
} from "./chunk-SLTQACJW.js";
|
|
@@ -235,7 +235,7 @@ async function flushTelemetry() {
|
|
|
235
235
|
try {
|
|
236
236
|
const res = await fetch(url, {
|
|
237
237
|
method: "POST",
|
|
238
|
-
headers:
|
|
238
|
+
headers: config.PROMPTOWL_FETCH_HEADERS,
|
|
239
239
|
body: JSON.stringify(payload)
|
|
240
240
|
});
|
|
241
241
|
console.log(`[telemetry] response: ${res.status} ${res.statusText}`);
|
|
@@ -261,8 +261,53 @@ function startTelemetryLoop() {
|
|
|
261
261
|
);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// src/db/settings.ts
|
|
265
|
+
var deployBaseline = null;
|
|
266
|
+
function norm(v) {
|
|
267
|
+
return v === void 0 || v === null ? null : v;
|
|
268
|
+
}
|
|
269
|
+
function baseline() {
|
|
270
|
+
if (!deployBaseline) deployBaseline = { ...process.env };
|
|
271
|
+
return deployBaseline;
|
|
272
|
+
}
|
|
273
|
+
async function persistSetting(name, value) {
|
|
274
|
+
const db = getDb();
|
|
275
|
+
const envAtWrite = norm(baseline()[name]);
|
|
276
|
+
await db.run(
|
|
277
|
+
insertOrReplace(
|
|
278
|
+
db,
|
|
279
|
+
`INSERT INTO server_settings (key, value, env_at_write, updated_at)
|
|
280
|
+
VALUES (?, ?, ?, ${nowExpr(db)})`,
|
|
281
|
+
["key"],
|
|
282
|
+
`value = excluded.value, env_at_write = excluded.env_at_write, updated_at = ${nowExpr(db)}`
|
|
283
|
+
),
|
|
284
|
+
[name, value, envAtWrite]
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
async function loadServerSettings() {
|
|
288
|
+
deployBaseline = { ...process.env };
|
|
289
|
+
const base = deployBaseline;
|
|
290
|
+
const db = getDb();
|
|
291
|
+
const rows = await db.all(
|
|
292
|
+
"SELECT key, value, env_at_write FROM server_settings"
|
|
293
|
+
);
|
|
294
|
+
let applied = 0;
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
const envVal = norm(base[row.key]);
|
|
297
|
+
if (envVal !== norm(row.env_at_write)) {
|
|
298
|
+
await persistSetting(row.key, envVal);
|
|
299
|
+
if (envVal === null) delete process.env[row.key];
|
|
300
|
+
else process.env[row.key] = envVal;
|
|
301
|
+
} else {
|
|
302
|
+
if (row.value === null) delete process.env[row.key];
|
|
303
|
+
else process.env[row.key] = row.value;
|
|
304
|
+
}
|
|
305
|
+
applied++;
|
|
306
|
+
}
|
|
307
|
+
return applied;
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
// src/auth/license.ts
|
|
265
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
266
311
|
var currentLicense = null;
|
|
267
312
|
function getCurrentLicense() {
|
|
268
313
|
return currentLicense;
|
|
@@ -284,21 +329,6 @@ async function isLicenseAdminUserId(userId) {
|
|
|
284
329
|
return false;
|
|
285
330
|
}
|
|
286
331
|
}
|
|
287
|
-
function upsertEnvVar(filePath, varName, value) {
|
|
288
|
-
const prefix = `${varName}=`;
|
|
289
|
-
let lines = [];
|
|
290
|
-
if (existsSync2(filePath)) {
|
|
291
|
-
lines = readFileSync2(filePath, "utf8").split(/\r?\n/);
|
|
292
|
-
}
|
|
293
|
-
const filtered = lines.filter((line) => !line.trimStart().startsWith(prefix));
|
|
294
|
-
if (value !== null) {
|
|
295
|
-
filtered.push(`${prefix}${value}`);
|
|
296
|
-
}
|
|
297
|
-
while (filtered.length && filtered[filtered.length - 1] === "") {
|
|
298
|
-
filtered.pop();
|
|
299
|
-
}
|
|
300
|
-
writeFileSync2(filePath, filtered.join("\n") + "\n", "utf8");
|
|
301
|
-
}
|
|
302
332
|
async function installLicenseKey(key) {
|
|
303
333
|
const trimmed = key.trim();
|
|
304
334
|
if (!trimmed.startsWith("pk_")) {
|
|
@@ -315,12 +345,12 @@ async function installLicenseKey(key) {
|
|
|
315
345
|
return { ...info, persisted: false };
|
|
316
346
|
}
|
|
317
347
|
try {
|
|
318
|
-
|
|
348
|
+
await persistSetting("PROMPTOWL_KEY", trimmed);
|
|
319
349
|
return { ...info, persisted: true };
|
|
320
350
|
} catch (err) {
|
|
321
351
|
const persistError = err instanceof Error ? err.message : String(err);
|
|
322
352
|
console.warn(
|
|
323
|
-
`[license] validated but FAILED to persist to
|
|
353
|
+
`[license] validated but FAILED to persist to the database: ${persistError}`
|
|
324
354
|
);
|
|
325
355
|
return { ...info, persisted: false, persistError };
|
|
326
356
|
}
|
|
@@ -360,12 +390,12 @@ async function handleLicenseRevoked() {
|
|
|
360
390
|
console.warn("[license] failed to wipe sessions:", err);
|
|
361
391
|
}
|
|
362
392
|
try {
|
|
363
|
-
|
|
393
|
+
await persistSetting("PROMPTOWL_KEY", null);
|
|
364
394
|
console.warn(
|
|
365
|
-
|
|
395
|
+
"[license] revoked \u2014 cleared persisted PROMPTOWL_KEY from the database"
|
|
366
396
|
);
|
|
367
397
|
} catch (err) {
|
|
368
|
-
console.warn("[license] failed to
|
|
398
|
+
console.warn("[license] failed to clear persisted key:", err);
|
|
369
399
|
}
|
|
370
400
|
process.env.PROMPTOWL_KEY = "";
|
|
371
401
|
currentLicense = {
|
|
@@ -430,7 +460,7 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
430
460
|
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
431
461
|
const res = await fetch(`${promptowlUrl}/api/license/validate`, {
|
|
432
462
|
method: "POST",
|
|
433
|
-
headers:
|
|
463
|
+
headers: config.PROMPTOWL_FETCH_HEADERS,
|
|
434
464
|
body: JSON.stringify({ key })
|
|
435
465
|
});
|
|
436
466
|
if (!res.ok) {
|
|
@@ -890,6 +920,12 @@ async function buildTitleMap(nestId) {
|
|
|
890
920
|
function folderForNode(nodeId) {
|
|
891
921
|
return nodeId.replace(/^nodes\//, "").split("/").slice(0, -1).join("/");
|
|
892
922
|
}
|
|
923
|
+
async function describeNode(nestId, nodeId) {
|
|
924
|
+
const titles = await buildTitleMap(nestId);
|
|
925
|
+
const title = titles.get(nodeId) || nodeId;
|
|
926
|
+
const folder = folderForNode(nodeId);
|
|
927
|
+
return folder ? `${title} (in ${folder})` : title;
|
|
928
|
+
}
|
|
893
929
|
async function nestName(nestId) {
|
|
894
930
|
try {
|
|
895
931
|
const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
|
|
@@ -1219,13 +1255,22 @@ async function createStewardRecord(params) {
|
|
|
1219
1255
|
"SELECT id FROM users WHERE email = ?",
|
|
1220
1256
|
[email]
|
|
1221
1257
|
);
|
|
1258
|
+
let userId = userRow?.id;
|
|
1259
|
+
if (!userId) {
|
|
1260
|
+
const { hashPassword } = await import("./keys-73STFJJB.js");
|
|
1261
|
+
userId = uuid2();
|
|
1262
|
+
await db.run(
|
|
1263
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1264
|
+
[userId, email, null, await hashPassword(uuid2())]
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1222
1267
|
const created = await assignSteward({
|
|
1223
1268
|
nestId: params.nestId,
|
|
1224
1269
|
scope: params.scope,
|
|
1225
1270
|
nodePattern,
|
|
1226
1271
|
tagName,
|
|
1227
1272
|
userEmail: email,
|
|
1228
|
-
userId
|
|
1273
|
+
userId,
|
|
1229
1274
|
role: user.role ?? "reviewer",
|
|
1230
1275
|
assignedBy: params.assignedBy,
|
|
1231
1276
|
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1541,10 +1586,11 @@ function rowToSteward(row) {
|
|
|
1541
1586
|
export {
|
|
1542
1587
|
trackEvent,
|
|
1543
1588
|
startTelemetryLoop,
|
|
1589
|
+
persistSetting,
|
|
1590
|
+
loadServerSettings,
|
|
1544
1591
|
getCurrentLicense,
|
|
1545
1592
|
isLicenseAdminEmail,
|
|
1546
1593
|
isLicenseAdminUserId,
|
|
1547
|
-
upsertEnvVar,
|
|
1548
1594
|
installLicenseKey,
|
|
1549
1595
|
startLicenseSafetyPoll,
|
|
1550
1596
|
isSuspended,
|
|
@@ -1573,6 +1619,7 @@ export {
|
|
|
1573
1619
|
deleteNest,
|
|
1574
1620
|
engineCache,
|
|
1575
1621
|
buildTitleMap,
|
|
1622
|
+
describeNode,
|
|
1576
1623
|
nestName,
|
|
1577
1624
|
docLink,
|
|
1578
1625
|
buildDocContext,
|