@promptowl/contextnest-community 0.1.0-alpha.2 → 1.0.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/CONFIGURATION.md CHANGED
@@ -1,118 +1,118 @@
1
- # Configuration
2
-
3
- All server configuration is driven by environment variables. Set them in your shell, in a `.env` file, in a Docker image, or via your process manager — however you deploy the rest of your stack.
4
-
5
- ## Auth modes
6
-
7
- The server supports two authentication modes. You pick one per instance with `AUTH_MODE`.
8
-
9
- ### `AUTH_MODE=key` (default)
10
-
11
- Every request requires an `Authorization: Bearer cnst_...` header. Use this for any multi-user or internet-facing deployment.
12
-
13
- - Users register via `POST /auth/register` (email + password) or log in with PromptOwl via `POST /auth/device` → `POST /auth/promptowl`.
14
- - API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use.
15
- - Rate-limited login / register / device-auth endpoints (sliding window, per IP + per email).
16
- - First PromptOwl-authenticated user becomes the server admin (atomic claim). Admin can invite teammates at `POST /auth/invite`.
17
-
18
- Clients include the token on every request:
19
-
20
- ```bash
21
- curl -H 'Authorization: Bearer cnst_<your-token>' http://your-server/nests
22
- ```
23
-
24
- ### `AUTH_MODE=open`
25
-
26
- No authentication required. Every request is attributed to the anonymous admin user (`00000000-...`). Everyone effectively owns every anon-created nest.
27
-
28
- **Use this only when:**
29
- - You're running locally for yourself (`bind 127.0.0.1`), or
30
- - You're on a trusted LAN and don't care about access controls, or
31
- - You're behind an upstream reverse proxy that already authenticates.
32
-
33
- **Don't use this when:**
34
- - The port is reachable from the public internet
35
- - Multiple people share the deployment and need isolated data
36
- - You need audit trails (every write shows up under "anonymous admin")
37
-
38
- The server prints a loud warning at startup when `AUTH_MODE=open` is active.
39
-
40
- ---
41
-
42
- ## Full env var reference
43
-
44
- | Var | Default | Purpose |
45
- |---|---|---|
46
- | `PORT` | `3838` | HTTP port the server listens on. |
47
- | `DATA_ROOT` | `./data` (relative to cwd) | Root directory for SQLite DB + nest filesystem vaults. Set to an absolute path in production so it's independent of where you `cd`'d from. |
48
- | `DATABASE_PATH` | `$DATA_ROOT/community.db` | Override the SQLite file location explicitly. |
49
- | `AUTH_MODE` | `key` | `key` or `open`. See above. |
50
- | `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. |
51
- | `PROMPTOWL_KEY` | `""` | Your PromptOwl Community Server license key (`pk_...`). Unlicensed instances still run but some features may be limited. |
52
- | `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
53
- | `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
54
- | `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). |
55
- | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
56
-
57
- ---
58
-
59
- ## Typical deployments
60
-
61
- ### Local dev / single user
62
-
63
- ```bash
64
- AUTH_MODE=open DATA_ROOT=./my-data pnpm dev
65
- ```
66
-
67
- Or the default dev mode if `pnpm dev` already sets `AUTH_MODE=open` in your scripts.
68
-
69
- ### Team / multi-user behind a reverse proxy
70
-
71
- ```bash
72
- AUTH_MODE=key \
73
- CORS_ORIGINS="https://team.example.com,https://admin.example.com" \
74
- DATA_ROOT=/var/lib/contextnest \
75
- PROMPTOWL_KEY=pk_... \
76
- pnpm start
77
- ```
78
-
79
- Terminate TLS at the proxy, forward `X-Forwarded-For` and `X-Real-IP` headers (the rate limiter reads them), and bind the server to `127.0.0.1` so only the proxy can reach it.
80
-
81
- ### Hosted / commercial SaaS
82
-
83
- Same as team, plus:
84
- - Run N instances behind a load balancer sharing a network-attached `DATA_ROOT` (note: SQLite + shared filesystem is not a great long-term story — migrate to the MongoDB storage adapter when scaling beyond one box).
85
- - Set `TELEMETRY_ENABLED=true` and a valid `PROMPTOWL_KEY` so usage rolls up to PromptOwl.
86
- - Rotate keys regularly via `DELETE /auth/keys/:id` + `POST /auth/keys`.
87
-
88
- ---
89
-
90
- ## access.yaml (optional, open + key mode)
91
-
92
- Drop an `access.yaml` at `$DATA_ROOT/access.yaml` to add a server-level ABAC layer — useful even in key mode for super-admins and group-based defaults.
93
-
94
- ```yaml
95
- mode: restricted
96
- allowed_users:
97
- - "*.acme.com" # email wildcard — anyone @acme.com
98
- - "partner@vendor.com" # exact match
99
- super_admins:
100
- - "ceo@acme.com" # always allowed on every nest
101
- groups:
102
- engineering:
103
- default_permission: write
104
- members:
105
- - "*.eng.acme.com"
106
- viewers:
107
- default_permission: read
108
- members:
109
- - "*.contractor.acme.com"
110
- ```
111
-
112
- See `STEWARDSHIP.md` for how super-admins and groups interact with per-nest stewardship.
113
-
114
- ---
115
-
116
- ## Reload
117
-
118
- Changing env vars requires a restart — the server reads them at boot. `tsx watch` in dev will pick up code changes automatically but not env changes.
1
+ # Configuration
2
+
3
+ All server configuration is driven by environment variables. Set them in your shell, in a `.env` file, in a Docker image, or via your process manager — however you deploy the rest of your stack.
4
+
5
+ ## Auth modes
6
+
7
+ The server supports two authentication modes. You pick one per instance with `AUTH_MODE`.
8
+
9
+ ### `AUTH_MODE=key` (default)
10
+
11
+ Every request requires an `Authorization: Bearer cnst_...` header. Use this for any multi-user or internet-facing deployment.
12
+
13
+ - Users register via `POST /auth/register` (email + password) or log in with PromptOwl via `POST /auth/device` → `POST /auth/promptowl`.
14
+ - API keys are per-user (`cnst_<64-hex>`) and can be scoped to a single nest for service-account use.
15
+ - Rate-limited login / register / device-auth endpoints (sliding window, per IP + per email).
16
+ - First PromptOwl-authenticated user becomes the server admin (atomic claim). Admin can invite teammates at `POST /auth/invite`.
17
+
18
+ Clients include the token on every request:
19
+
20
+ ```bash
21
+ curl -H 'Authorization: Bearer cnst_<your-token>' http://your-server/nests
22
+ ```
23
+
24
+ ### `AUTH_MODE=open`
25
+
26
+ No authentication required. Every request is attributed to the anonymous admin user (`00000000-...`). Everyone effectively owns every anon-created nest.
27
+
28
+ **Use this only when:**
29
+ - You're running locally for yourself (`bind 127.0.0.1`), or
30
+ - You're on a trusted LAN and don't care about access controls, or
31
+ - You're behind an upstream reverse proxy that already authenticates.
32
+
33
+ **Don't use this when:**
34
+ - The port is reachable from the public internet
35
+ - Multiple people share the deployment and need isolated data
36
+ - You need audit trails (every write shows up under "anonymous admin")
37
+
38
+ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
39
+
40
+ ---
41
+
42
+ ## Full env var reference
43
+
44
+ | Var | Default | Purpose |
45
+ |---|---|---|
46
+ | `PORT` | `3838` | HTTP port the server listens on. |
47
+ | `DATA_ROOT` | `./data` (relative to cwd) | Root directory for SQLite DB + nest filesystem vaults. Set to an absolute path in production so it's independent of where you `cd`'d from. |
48
+ | `DATABASE_PATH` | `$DATA_ROOT/community.db` | Override the SQLite file location explicitly. |
49
+ | `AUTH_MODE` | `key` | `key` or `open`. See above. |
50
+ | `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. |
51
+ | `PROMPTOWL_KEY` | `""` | Your PromptOwl Community Server license key (`pk_...`). Unlicensed instances still run but some features may be limited. |
52
+ | `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
53
+ | `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
54
+ | `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). |
55
+ | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
56
+
57
+ ---
58
+
59
+ ## Typical deployments
60
+
61
+ ### Local dev / single user
62
+
63
+ ```bash
64
+ AUTH_MODE=open DATA_ROOT=./my-data pnpm dev
65
+ ```
66
+
67
+ Or the default dev mode if `pnpm dev` already sets `AUTH_MODE=open` in your scripts.
68
+
69
+ ### Team / multi-user behind a reverse proxy
70
+
71
+ ```bash
72
+ AUTH_MODE=key \
73
+ CORS_ORIGINS="https://team.example.com,https://admin.example.com" \
74
+ DATA_ROOT=/var/lib/contextnest \
75
+ PROMPTOWL_KEY=pk_... \
76
+ pnpm start
77
+ ```
78
+
79
+ Terminate TLS at the proxy, forward `X-Forwarded-For` and `X-Real-IP` headers (the rate limiter reads them), and bind the server to `127.0.0.1` so only the proxy can reach it.
80
+
81
+ ### Hosted / commercial SaaS
82
+
83
+ Same as team, plus:
84
+ - Run N instances behind a load balancer sharing a network-attached `DATA_ROOT` (note: SQLite + shared filesystem is not a great long-term story — migrate to the MongoDB storage adapter when scaling beyond one box).
85
+ - Set `TELEMETRY_ENABLED=true` and a valid `PROMPTOWL_KEY` so usage rolls up to PromptOwl.
86
+ - Rotate keys regularly via `DELETE /auth/keys/:id` + `POST /auth/keys`.
87
+
88
+ ---
89
+
90
+ ## access.yaml (optional, open + key mode)
91
+
92
+ Drop an `access.yaml` at `$DATA_ROOT/access.yaml` to add a server-level ABAC layer — useful even in key mode for super-admins and group-based defaults.
93
+
94
+ ```yaml
95
+ mode: restricted
96
+ allowed_users:
97
+ - "*.acme.com" # email wildcard — anyone @acme.com
98
+ - "partner@vendor.com" # exact match
99
+ super_admins:
100
+ - "ceo@acme.com" # always allowed on every nest
101
+ groups:
102
+ engineering:
103
+ default_permission: write
104
+ members:
105
+ - "*.eng.acme.com"
106
+ viewers:
107
+ default_permission: read
108
+ members:
109
+ - "*.contractor.acme.com"
110
+ ```
111
+
112
+ See `STEWARDSHIP.md` for how super-admins and groups interact with per-nest stewardship.
113
+
114
+ ---
115
+
116
+ ## Reload
117
+
118
+ Changing env vars requires a restart — the server reads them at boot. `tsx watch` in dev will pick up code changes automatically but not env changes.
@@ -1,13 +1,74 @@
1
1
  import {
2
2
  canUserApprove,
3
3
  resolveStewardsForNode
4
- } from "./chunk-Q2DCOS7V.js";
4
+ } from "./chunk-K22GWPT4.js";
5
5
  import {
6
+ createVersion,
7
+ setApprovedVersion
8
+ } from "./chunk-JMZ75ZCD.js";
9
+ import {
10
+ config,
6
11
  getDb
7
- } from "./chunk-USIDOGVJ.js";
12
+ } from "./chunk-KQCWNHDM.js";
8
13
 
9
14
  // src/governance/review-service.ts
10
15
  import { v4 as uuid } from "uuid";
16
+
17
+ // src/nodes/engine.ts
18
+ import { join } from "path";
19
+ import {
20
+ NestStorage,
21
+ GraphQueryEngine,
22
+ VersionManager
23
+ } from "@promptowl/contextnest-engine";
24
+ var NestEngineCache = class {
25
+ cache = /* @__PURE__ */ new Map();
26
+ get(nestId) {
27
+ let engine = this.cache.get(nestId);
28
+ if (!engine) {
29
+ const nestPath = join(config.DATA_ROOT, "nests", nestId);
30
+ const storage = new NestStorage(nestPath);
31
+ const query = new GraphQueryEngine(storage);
32
+ const versions = new VersionManager(storage);
33
+ engine = { storage, query, versions };
34
+ this.cache.set(nestId, engine);
35
+ }
36
+ return engine;
37
+ }
38
+ evict(nestId) {
39
+ this.cache.delete(nestId);
40
+ }
41
+ };
42
+ var engineCache = new NestEngineCache();
43
+
44
+ // src/governance/safe-publish.ts
45
+ import {
46
+ publishDocument,
47
+ serializeDocument
48
+ } from "@promptowl/contextnest-engine";
49
+ async function safePublishDocument(storage, docId, options) {
50
+ const node = await storage.readDocument(docId);
51
+ const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
52
+ const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
53
+ await storage.writeDocument(docId, serializeDocument(cleanedNode));
54
+ return publishDocument(storage, docId, options);
55
+ }
56
+ function stripUndefinedDeep(value) {
57
+ if (Array.isArray(value)) {
58
+ return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
59
+ }
60
+ if (value && typeof value === "object") {
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(value)) {
63
+ if (v === void 0) continue;
64
+ out[k] = stripUndefinedDeep(v);
65
+ }
66
+ return out;
67
+ }
68
+ return value;
69
+ }
70
+
71
+ // src/governance/review-service.ts
11
72
  function submitForReview(params) {
12
73
  const db = getDb();
13
74
  const existing = db.prepare(
@@ -35,7 +96,7 @@ function submitForReview(params) {
35
96
  ).run(params.nestId, params.nodeId, params.version);
36
97
  return getReviewRequest(id);
37
98
  }
38
- function approve(params) {
99
+ async function approve(params) {
39
100
  const db = getDb();
40
101
  const pending = db.prepare(
41
102
  "SELECT * FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending' ORDER BY requested_at DESC LIMIT 1"
@@ -65,12 +126,45 @@ function approve(params) {
65
126
  pending.id
66
127
  );
67
128
  db.prepare(
68
- "UPDATE node_versions SET status = 'approved' WHERE nest_id = ? AND node_id = ? AND version = ?"
129
+ "UPDATE node_versions SET status = 'published' WHERE nest_id = ? AND node_id = ? AND version = ?"
69
130
  ).run(params.nestId, params.nodeId, params.version);
70
131
  db.prepare(
71
132
  `INSERT OR REPLACE INTO approved_versions (nest_id, node_id, approved_version, approved_by)
72
133
  VALUES (?, ?, ?, ?)`
73
134
  ).run(params.nestId, params.nodeId, params.version, params.approvedBy);
135
+ try {
136
+ const { storage } = engineCache.get(params.nestId);
137
+ const result = await safePublishDocument(storage, params.nodeId, {
138
+ editedBy: params.approvedBy,
139
+ note: params.note || `Approved review request ${pending.id}`
140
+ });
141
+ const engineVersion = result.versionEntry.version;
142
+ if (engineVersion !== params.version) {
143
+ const node = result.node;
144
+ const tags = node.frontmatter.tags || [];
145
+ createVersion({
146
+ nestId: params.nestId,
147
+ nodeId: params.nodeId,
148
+ version: engineVersion,
149
+ content: node.body || "",
150
+ author: params.approvedBy,
151
+ status: "published",
152
+ tags,
153
+ changeNote: params.note || `Approved review request ${pending.id}`
154
+ });
155
+ setApprovedVersion(
156
+ params.nestId,
157
+ params.nodeId,
158
+ engineVersion,
159
+ params.approvedBy
160
+ );
161
+ }
162
+ } catch (err) {
163
+ console.error(
164
+ `publishDocument failed for ${params.nestId}/${params.nodeId} on approve:`,
165
+ err
166
+ );
167
+ }
74
168
  return getReviewRequest(pending.id);
75
169
  }
76
170
  function reject(params) {
@@ -189,6 +283,8 @@ function rowToReviewRequest(row) {
189
283
  }
190
284
 
191
285
  export {
286
+ engineCache,
287
+ safePublishDocument,
192
288
  submitForReview,
193
289
  approve,
194
290
  reject,
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  getDb
3
- } from "./chunk-USIDOGVJ.js";
3
+ } from "./chunk-KQCWNHDM.js";
4
4
 
5
5
  // src/governance/version-service.ts
6
6
  import { createHash } from "crypto";
7
7
  function hashContent(content) {
8
8
  return createHash("sha256").update(content).digest("hex");
9
9
  }
10
+ var SYSTEM_AUTHOR_PREFIX = "system:auto-publish:";
11
+ function systemAuthor(email) {
12
+ return `${SYSTEM_AUTHOR_PREFIX}${email}`;
13
+ }
10
14
  function createVersion(params) {
11
15
  const db = getDb();
12
16
  const contentHash = hashContent(params.content);
@@ -49,8 +53,10 @@ function getVersion(nestId, nodeId, version) {
49
53
  function getCurrentVersion(nestId, nodeId) {
50
54
  const db = getDb();
51
55
  const row = db.prepare(
52
- "SELECT MAX(version) as v FROM node_versions WHERE nest_id = ? AND node_id = ?"
53
- ).get(nestId, nodeId);
56
+ `SELECT MAX(version) as v FROM node_versions
57
+ WHERE nest_id = ? AND node_id = ?
58
+ AND author NOT LIKE ?`
59
+ ).get(nestId, nodeId, `${SYSTEM_AUTHOR_PREFIX}%`);
54
60
  return row?.v || 0;
55
61
  }
56
62
  function getApprovedVersion(nestId, nodeId) {
@@ -70,8 +76,13 @@ function setApprovedVersion(nestId, nodeId, version, approvedBy) {
70
76
  function checkConflict(nestId, nodeId, baseVersion) {
71
77
  const db = getDb();
72
78
  const current = db.prepare(
73
- "SELECT version, content_hash, author, created_at FROM node_versions WHERE nest_id = ? AND node_id = ? ORDER BY version DESC LIMIT 1"
74
- ).get(nestId, nodeId);
79
+ `SELECT version, content_hash, author, created_at
80
+ FROM node_versions
81
+ WHERE nest_id = ? AND node_id = ?
82
+ AND author NOT LIKE ?
83
+ ORDER BY version DESC
84
+ LIMIT 1`
85
+ ).get(nestId, nodeId, `${SYSTEM_AUTHOR_PREFIX}%`);
75
86
  if (!current) {
76
87
  return { conflict: false, currentVersion: 0, currentHash: "" };
77
88
  }
@@ -102,6 +113,30 @@ function getNodeTags(nestId, nodeId) {
102
113
  return [];
103
114
  }
104
115
  }
116
+ function getDisplayStatus(nestId, nodeId) {
117
+ const db = getDb();
118
+ const pending = db.prepare(
119
+ "SELECT 1 FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending' LIMIT 1"
120
+ ).get(nestId, nodeId);
121
+ if (pending) return "pending_review";
122
+ const current = db.prepare(
123
+ `SELECT version, status FROM node_versions
124
+ WHERE nest_id = ? AND node_id = ?
125
+ ORDER BY version DESC LIMIT 1`
126
+ ).get(nestId, nodeId);
127
+ if (!current) return "draft";
128
+ if (current.status === "published" || current.status === "approved") {
129
+ const approved = db.prepare(
130
+ "SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?"
131
+ ).get(nestId, nodeId);
132
+ if (approved?.approved_version === current.version) {
133
+ return current.status === "published" ? "published" : "approved";
134
+ }
135
+ return "draft";
136
+ }
137
+ if (current.status === "rejected") return "rejected";
138
+ return "draft";
139
+ }
105
140
  function rowToVersion(row) {
106
141
  return {
107
142
  version: row.version,
@@ -116,6 +151,8 @@ function rowToVersion(row) {
116
151
 
117
152
  export {
118
153
  hashContent,
154
+ SYSTEM_AUTHOR_PREFIX,
155
+ systemAuthor,
119
156
  createVersion,
120
157
  getVersions,
121
158
  getVersion,
@@ -123,5 +160,6 @@ export {
123
160
  getApprovedVersion,
124
161
  setApprovedVersion,
125
162
  checkConflict,
126
- getNodeTags
163
+ getNodeTags,
164
+ getDisplayStatus
127
165
  };