@emseepea/create-multi-instance-postgres-server 0.0.4 → 0.0.8

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 CHANGED
@@ -25,6 +25,30 @@ ordinary tests, and semantic tests.
25
25
 
26
26
  <!-- generated-project-readme -->
27
27
 
28
+ ## Choose Open or Protected Access
29
+
30
+ Start open when the catalogue and operations are public.
31
+
32
+ To protect this template, pass both options to the app factory:
33
+
34
+ - `access: { access: "protected", requiredScopes: ["peas:read"] }`
35
+ - an `authentication` adapter
36
+
37
+ Keep `authentication.discovery` as `"public"` unless capability names or
38
+ schemas are sensitive. Use `"protected"` only when each principal should see a
39
+ permission-filtered catalogue. OAuth metadata remains public in both modes.
40
+
41
+ ## Add Observability
42
+
43
+ The same factory accepts `observability`.
44
+
45
+ - Use `structuredLogging` for safe structured events.
46
+ - Use `openTelemetry` for traces and metrics.
47
+
48
+ Adapters receive only redacted framework events. They never receive request
49
+ bodies, arguments, results, tokens, provider claims, or raw errors. See the
50
+ [server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
51
+
28
52
  ## Multi-Instance PostgreSQL Server
29
53
 
30
54
  Each server process owns its own Em See Pea app and bounded PostgreSQL
@@ -93,6 +117,13 @@ to the shared database continues to serve the same state.
93
117
  - No claim of exactly-once external effects, latency, throughput, or unlimited
94
118
  scale.
95
119
 
120
+ ## Add Feedback
121
+
122
+ Install `@emseepea/feedback` when this server needs a detailed one-way
123
+ observation or a durable support conversation. Pass its tools through the
124
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
125
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
126
+
96
127
  ## Check This Project
97
128
 
98
129
  [Ordinary tests](test/) prove cross-process state, complete-state replacement,
@@ -1,4 +1,28 @@
1
- # Multi-Instance PostgreSQL Server
1
+ # Choose Open or Protected Access
2
+
3
+ Start open when the catalogue and operations are public.
4
+
5
+ To protect this template, pass both options to the app factory:
6
+
7
+ - `access: { access: "protected", requiredScopes: ["peas:read"] }`
8
+ - an `authentication` adapter
9
+
10
+ Keep `authentication.discovery` as `"public"` unless capability names or
11
+ schemas are sensitive. Use `"protected"` only when each principal should see a
12
+ permission-filtered catalogue. OAuth metadata remains public in both modes.
13
+
14
+ ## Add Observability
15
+
16
+ The same factory accepts `observability`.
17
+
18
+ - Use `structuredLogging` for safe structured events.
19
+ - Use `openTelemetry` for traces and metrics.
20
+
21
+ Adapters receive only redacted framework events. They never receive request
22
+ bodies, arguments, results, tokens, provider claims, or raw errors. See the
23
+ [server API](https://github.com/emseepea/emseepea/tree/main/packages/framework#authentication-and-observability) for the complete configuration.
24
+
25
+ ## Multi-Instance PostgreSQL Server
2
26
 
3
27
  Each server process owns its own Em See Pea app and bounded PostgreSQL
4
28
  connection pool. Every process reads and writes the same application state, so
@@ -66,6 +90,13 @@ to the shared database continues to serve the same state.
66
90
  - No claim of exactly-once external effects, latency, throughput, or unlimited
67
91
  scale.
68
92
 
93
+ ## Add Feedback
94
+
95
+ Install `@emseepea/feedback` when this server needs a detailed one-way
96
+ observation or a durable support conversation. Pass its tools through the
97
+ application factory's `additionalTools` option. Choose PostgreSQL, Firestore,
98
+ GitHub Issues, or Zendesk in the [feedback guide](../../packages/feedback/README.md).
99
+
69
100
  ## Check This Project
70
101
 
71
102
  [Ordinary tests](test/) prove cross-process state, complete-state replacement,
@@ -6,7 +6,7 @@ services:
6
6
  POSTGRES_PASSWORD: emseepea
7
7
  POSTGRES_USER: emseepea
8
8
  healthcheck:
9
- test: ["CMD-SHELL", "pg_isready -U emseepea -d emseepea"]
9
+ test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U emseepea -d emseepea"]
10
10
  interval: 1s
11
11
  timeout: 3s
12
12
  retries: 20
@@ -1,18 +1,26 @@
1
1
  import test from "node:test";
2
2
  import {
3
+ assertNoNegativeFeedback,
3
4
  assertResponseContains,
4
5
  assertResponseMeaning,
5
- assertToolCalls,
6
+ assertToolCallsWithOptionalFeedback,
6
7
  createConversation,
7
8
  } from "@emseepea/testing/semantic";
8
9
 
10
+ const server = new URL(import.meta.resolve("@emseepea/feedback/testing-server"));
11
+
9
12
  const databaseUrl = process.env.DATABASE_URL;
10
13
  if (!databaseUrl) throw new Error("DATABASE_URL is required for the PostgreSQL semantic test");
11
14
 
12
15
  test("saves and retrieves a harvest report without exposing server instances", async (t) => {
13
16
  const chat = await createConversation(t, {
14
- server: new URL("../dist/server.js", import.meta.url),
15
- environment: { DATABASE_URL: databaseUrl },
17
+ server,
18
+ environment: {
19
+ DATABASE_URL: databaseUrl,
20
+ EMSEEPEA_EVAL_APP_MODULE: new URL("../dist/app.js", import.meta.url).href,
21
+ EMSEEPEA_EVAL_APP_FACTORY: "createMultiInstanceExample",
22
+ EMSEEPEA_EVAL_APP_KIND: "postgres",
23
+ },
16
24
  });
17
25
 
18
26
  // Two natural turns cover write and read tool selection at low model cost.
@@ -22,7 +30,7 @@ test("saves and retrieves a harvest report without exposing server instances", a
22
30
  "Save a harvest report for North Bed on 2026-09-08 with 12 shelling pea " +
23
31
  "plants and 8 snap pea plants.",
24
32
  );
25
- assertToolCalls(saved, [
33
+ await assertToolCallsWithOptionalFeedback(saved, [
26
34
  {
27
35
  name: "save-harvest-report",
28
36
  arguments: {
@@ -38,7 +46,7 @@ test("saves and retrieves a harvest report without exposing server instances", a
38
46
  const retrieved = await chat.send(
39
47
  "What harvest report do we have for that garden bed and date?",
40
48
  );
41
- assertToolCalls(retrieved, [
49
+ await assertToolCallsWithOptionalFeedback(retrieved, [
42
50
  {
43
51
  name: "get-harvest-report",
44
52
  arguments: { gardenBed: "North Bed", harvestDate: "2026-09-08" },
@@ -49,4 +57,5 @@ test("saves and retrieves a harvest report without exposing server instances", a
49
57
  "The saved report for North Bed on 2026-09-08 has 12 shelling pea plants, " +
50
58
  "8 snap pea plants, and 20 plants in total.",
51
59
  });
60
+ assertNoNegativeFeedback(saved, retrieved);
52
61
  });
@@ -17,7 +17,8 @@
17
17
  "lint": "oxlint src test eval"
18
18
  },
19
19
  "devDependencies": {
20
- "@emseepea/testing": "0.5.3",
20
+ "@emseepea/feedback": "0.2.0",
21
+ "@emseepea/testing": "0.9.3",
21
22
  "@modelcontextprotocol/client": "2.0.0",
22
23
  "@types/node": "24.13.3",
23
24
  "@types/pg": "8.23.1",
@@ -29,7 +30,7 @@
29
30
  },
30
31
  "private": true,
31
32
  "dependencies": {
32
- "@emseepea/server": "0.3.3",
33
+ "@emseepea/server": "0.6.1",
33
34
  "pg": "8.23.0",
34
35
  "zod": "4.4.3"
35
36
  }
@@ -1,16 +1,23 @@
1
- import { createEmseepea, discoverCapabilities } from "@emseepea/server";
1
+ import {
2
+ createEmseepea,
3
+ discoverCapabilities,
4
+ type AccessPolicy,
5
+ type EmseepeaExtensions,
6
+ } from "@emseepea/server";
2
7
  import { Pool } from "pg";
3
8
  import { z } from "zod";
4
9
 
5
- export interface MultiInstanceExampleOptions {
10
+ export interface MultiInstanceExampleOptions extends EmseepeaExtensions {
6
11
  readonly databaseUrl: string;
12
+ readonly access?: AccessPolicy;
7
13
  }
8
14
 
9
15
  export async function createMultiInstanceExample(options: MultiInstanceExampleOptions) {
16
+ const { databaseUrl: databaseUrlOption, access = { access: "public" }, ...extensions } = options;
10
17
  const databaseUrl = z.string().url().refine(
11
18
  (value) => ["postgres:", "postgresql:"].includes(new URL(value).protocol),
12
19
  "databaseUrl must use PostgreSQL",
13
- ).parse(options.databaseUrl);
20
+ ).parse(databaseUrlOption);
14
21
  let database: Pool | undefined = new Pool({
15
22
  connectionString: databaseUrl,
16
23
  connectionTimeoutMillis: 2_000,
@@ -41,7 +48,9 @@ export async function createMultiInstanceExample(options: MultiInstanceExampleOp
41
48
  readinessTimeoutMs: 2_500,
42
49
  ...await discoverCapabilities(new URL("./capabilities/", import.meta.url), {
43
50
  database: () => database,
51
+ access,
44
52
  }),
53
+ ...extensions,
45
54
  });
46
55
  const closeProvider = async () => {
47
56
  const activeDatabase = database;
@@ -1,5 +1,7 @@
1
1
  import type { Pool } from "pg";
2
+ import type { AccessPolicy } from "@emseepea/server";
2
3
 
3
4
  export interface MultiInstanceContext {
4
5
  readonly database: () => Pool | undefined;
6
+ readonly access: AccessPolicy;
5
7
  }
@@ -1,4 +1,4 @@
1
- import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
1
+ import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
2
2
  import { z } from "zod";
3
3
  import { reportKeySchema, reportSchema } from "../harvest-report.js";
4
4
  import type { MultiInstanceContext } from "./context.js";
@@ -12,11 +12,11 @@ const outputSchema = z.object({
12
12
 
13
13
  export default ((context) => defineTool({
14
14
  name: "get-harvest-report",
15
- access: "public",
15
+ ...context.access,
16
16
  description: "Get the pea harvest report for a garden bed and date.",
17
17
  inputSchema,
18
18
  outputSchema,
19
- async handler({ gardenBed, harvestDate }, { signal }) {
19
+ async handler({ gardenBed, harvestDate }, { signal }: ToolContext) {
20
20
  const database = context.database();
21
21
  if (!database) throw new Error("Report provider unavailable");
22
22
  signal.throwIfAborted();
@@ -1,4 +1,4 @@
1
- import { defineTool, type CapabilityModuleFactory } from "@emseepea/server";
1
+ import { defineTool, type CapabilityModuleFactory, type ToolContext } from "@emseepea/server";
2
2
  import { z } from "zod";
3
3
  import { editableReportSchema, reportSchema } from "../harvest-report.js";
4
4
  import type { MultiInstanceContext } from "./context.js";
@@ -8,11 +8,11 @@ const outputSchema = reportSchema;
8
8
 
9
9
  export default ((context) => defineTool({
10
10
  name: "save-harvest-report",
11
- access: "public",
11
+ ...context.access,
12
12
  description: "Save the complete pea harvest report for a garden bed and date.",
13
13
  inputSchema,
14
14
  outputSchema,
15
- async handler({ gardenBed, harvestDate, shellingCount, snapCount }, { signal }) {
15
+ async handler({ gardenBed, harvestDate, shellingCount, snapCount }, { signal }: ToolContext) {
16
16
  const database = context.database();
17
17
  if (!database) throw new Error("Report provider unavailable");
18
18
  signal.throwIfAborted();
@@ -4,6 +4,8 @@ import { fileURLToPath } from "node:url";
4
4
  import test, { after } from "node:test";
5
5
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
6
6
  import { Pool } from "pg";
7
+ import { insecureTestAuthentication, startEmseepea } from "@emseepea/testing";
8
+ import { createMultiInstanceExample } from "../dist/app.js";
7
9
 
8
10
  const serverPath = fileURLToPath(new URL("../dist/server.js", import.meta.url));
9
11
  const databaseUrl = process.env.DATABASE_URL;
@@ -67,6 +69,25 @@ test("two interchangeable server processes share one coherent report store", asy
67
69
  assert.equal(await readiness.text(), "not ready\n");
68
70
  });
69
71
 
72
+ test("the same template composes protected access and observability", async (t) => {
73
+ const events = [];
74
+ const permissions = ["reports:write"];
75
+ const { app } = await createMultiInstanceExample({
76
+ databaseUrl,
77
+ access: { access: "protected", requiredScopes: permissions },
78
+ authentication: insecureTestAuthentication(permissions),
79
+ observability: [{ id: "test-log", emit: (event) => events.push(event) }],
80
+ });
81
+ const running = await startEmseepea(t, app);
82
+ const client = await running.connect("test-token");
83
+ const result = await client.callTool({
84
+ name: "get-harvest-report",
85
+ arguments: { gardenBed: "Missing", harvestDate: "2026-09-08" },
86
+ });
87
+ assert.equal(result.isError, false);
88
+ assert.ok(events.some(({ capability }) => capability === "get-harvest-report"));
89
+ });
90
+
70
91
  test("describes every public harvest report property", async (t) => {
71
92
  const instance = await startInstance("schema-instance", databaseUrl);
72
93
  const client = await connect(instance.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/create-multi-instance-postgres-server",
3
- "version": "0.0.4",
3
+ "version": "0.0.8",
4
4
  "description": "Create interchangeable Em See Pea server instances sharing PostgreSQL state.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,10 +21,11 @@
21
21
  "prepack": "npm run build:initializer"
22
22
  },
23
23
  "devDependencies": {
24
- "@emseepea/server": "0.3.3",
24
+ "@emseepea/feedback": "0.2.0",
25
+ "@emseepea/server": "0.6.1",
25
26
  "pg": "8.23.0",
26
27
  "zod": "4.4.3",
27
- "@emseepea/testing": "0.5.3",
28
+ "@emseepea/testing": "0.9.3",
28
29
  "@modelcontextprotocol/client": "2.0.0",
29
30
  "@types/node": "24.13.3",
30
31
  "@types/pg": "8.23.1",