@tpsdev-ai/flair 0.17.0 → 0.19.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.
@@ -1,12 +1,24 @@
1
1
  import { Resource } from "@harperfast/harper";
2
+ import { allowAdmin } from "./agent-auth.js";
2
3
  /**
3
4
  * GET /Admin — friendly redirect to /AdminDashboard.
4
5
  *
5
6
  * Operators bookmark or type the bare /Admin path. Without this resource
6
7
  * they hit a 404 and assume the admin UI is broken. The dashboard is the
7
8
  * canonical landing surface; redirect there.
9
+ *
10
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): the /Admin* pathname
11
+ * gate in auth-middleware.ts only 401s when there's NO Authorization header
12
+ * at all (or Basic creds don't resolve to a real user) — a validly-verified
13
+ * TPS-Ed25519 agent that is NOT an admin, or a valid-but-non-super_user Basic
14
+ * user, currently sails through the middleware with no admin resource-level
15
+ * check at all. allowRead closes that gap the same way WorkspaceLatest.ts /
16
+ * MemoryReindex.ts already gate their own custom (non-@table) Resources.
8
17
  */
9
18
  export class Admin extends Resource {
19
+ async allowRead() {
20
+ return allowAdmin(this.getContext?.());
21
+ }
10
22
  async get() {
11
23
  return new Response("", {
12
24
  status: 302,
@@ -1,9 +1,15 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
2
  import { layout, htmlResponse, esc } from "./admin-layout.js";
3
+ import { allowAdmin } from "./agent-auth.js";
3
4
  /**
4
5
  * GET /AdminConnectors — OAuth clients and active sessions.
6
+ *
7
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts.
5
8
  */
6
9
  export class AdminConnectors extends Resource {
10
+ async allowRead() {
11
+ return allowAdmin(this.getContext?.());
12
+ }
7
13
  async get() {
8
14
  const clients = [];
9
15
  try {
@@ -1,9 +1,18 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
2
  import { layout, htmlResponse } from "./admin-layout.js";
3
+ import { allowAdmin } from "./agent-auth.js";
3
4
  /**
4
5
  * GET /AdminDashboard — admin home page with system overview.
6
+ *
7
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts for why
8
+ * this closes a real gap, not just belt-and-suspenders — a verified
9
+ * non-admin agent could otherwise read full instance stats (principal/agent/
10
+ * memory/relationship counts) with no admin check at all.
5
11
  */
6
12
  export class AdminDashboard extends Resource {
13
+ async allowRead() {
14
+ return allowAdmin(this.getContext?.());
15
+ }
7
16
  async get() {
8
17
  let principalCount = 0;
9
18
  let memoryCount = 0;
@@ -1,9 +1,15 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
2
  import { layout, htmlResponse, esc } from "./admin-layout.js";
3
+ import { allowAdmin } from "./agent-auth.js";
3
4
  /**
4
5
  * GET /AdminIdp — enterprise IdP configuration management.
6
+ *
7
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts.
5
8
  */
6
9
  export class AdminIdp extends Resource {
10
+ async allowRead() {
11
+ return allowAdmin(this.getContext?.());
12
+ }
7
13
  async get() {
8
14
  const idps = [];
9
15
  try {
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { join, dirname } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { allowAdmin } from "./agent-auth.js";
7
8
  /**
8
9
  * Resolve the public URL operators reach this Flair on.
9
10
  *
@@ -83,8 +84,13 @@ function resolveVersion() {
83
84
  }
84
85
  /**
85
86
  * GET /AdminInstance — instance info, public key, version.
87
+ *
88
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts.
86
89
  */
87
90
  export class AdminInstance extends Resource {
91
+ async allowRead() {
92
+ return allowAdmin(this.getContext?.());
93
+ }
88
94
  async get() {
89
95
  const version = resolveVersion();
90
96
  // Pass the request through so URL resolution can derive from
@@ -1,5 +1,6 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
2
  import { layout, htmlResponse, esc } from "./admin-layout.js";
3
+ import { allowAdmin } from "./agent-auth.js";
3
4
  /**
4
5
  * GET /AdminMemory browse + search memories (list view)
5
6
  * GET /AdminMemory?id=<id> per-memory detail view with full provenance pane
@@ -19,8 +20,15 @@ import { layout, htmlResponse, esc } from "./admin-layout.js";
19
20
  * "memory that follows the agent across orchestrators" — the provenance pane
20
21
  * makes that legible: every memory shows where it came from, what it derived
21
22
  * from, what it superseded, and which peer it synced from.
23
+ *
24
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts — this
25
+ * page surfaces full memory content across ALL agents, so the gap it closes
26
+ * matters more here than on the other Admin* pages.
22
27
  */
23
28
  export class AdminMemory extends Resource {
29
+ async allowRead() {
30
+ return allowAdmin(this.getContext?.());
31
+ }
24
32
  async get() {
25
33
  // Harper v5 does not populate this.request on Resource subclasses —
26
34
  // getContext() is the only reliable path (ops-sal4: the previous
@@ -1,9 +1,15 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
2
  import { layout, htmlResponse, esc } from "./admin-layout.js";
3
+ import { allowAdmin } from "./agent-auth.js";
3
4
  /**
4
5
  * GET /AdminPrincipals — list all principals with kind, trust, status.
6
+ *
7
+ * allowRead()=allowAdmin (ops-oox7 defense-in-depth): see Admin.ts.
5
8
  */
6
9
  export class AdminPrincipals extends Resource {
10
+ async allowRead() {
11
+ return allowAdmin(this.getContext?.());
12
+ }
7
13
  async get() {
8
14
  const principals = [];
9
15
  try {
@@ -1,7 +1,8 @@
1
1
  import { databases } from "@harperfast/harper";
2
- import { resolveAgentAuth } from "./agent-auth.js";
2
+ import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
3
3
  const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
4
4
  const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
5
+ const NOT_FOUND = () => new Response(JSON.stringify({ error: "not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
5
6
  /**
6
7
  * Integration records are agent-owned. Auth: the non-rejecting gate annotates the
7
8
  * request; this resource self-enforces (resolveAgentAuth → internal/agent/anonymous).
@@ -12,6 +13,50 @@ export class Integration extends databases.flair.Integration {
12
13
  _auth() {
13
14
  return resolveAgentAuth(this.getContext?.());
14
15
  }
16
+ /**
17
+ * Self-authorize now that the global gate is non-rejecting (memory-soul-
18
+ * read-gate family fix, ops-oox7 — same pattern as Memory.ts/Soul.ts/
19
+ * WorkspaceState.ts/Relationship.ts). Closes the same P0 leak: Harper
20
+ * routes `GET /Integration/<id>` to get() and the collection describe
21
+ * (`GET /Integration`) outside search(), so neither was gated before this
22
+ * fix — an anonymous caller got a 200 with full record content. Per-record
23
+ * ownership scoping happens in get() below; the collection scope is still
24
+ * in search().
25
+ */
26
+ allowRead() { return allowVerified(this.getContext?.()); }
27
+ /**
28
+ * Override get() to scope by-id reads the same way search() scopes
29
+ * collection reads (memory-soul-read-gate family fix). Never distinguishes
30
+ * "doesn't exist" from "exists but not yours" — both return 404, never
31
+ * 403, so a denied caller can't use get() to enumerate other agents'
32
+ * integration ids.
33
+ */
34
+ async get(target) {
35
+ // Collection / query reads arrive as a RequestTarget with
36
+ // `isCollection === true`, and are governed by search() (same owner
37
+ // scoping). Only a genuine by-id get is ownership-checked below — see
38
+ // Memory.ts's get() for the full rationale (same bug class).
39
+ if (!target || (typeof target === "object" && target.isCollection)) {
40
+ return this.search(target);
41
+ }
42
+ const auth = await this._auth();
43
+ // Anonymous by-id read is already blocked at the allowRead() gate (403);
44
+ // this is defense-in-depth if get() is ever reached directly.
45
+ if (auth.kind === "anonymous") {
46
+ return NOT_FOUND();
47
+ }
48
+ // Trusted internal call or admin agent — unfiltered, unchanged behavior.
49
+ if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
50
+ return super.get(target);
51
+ }
52
+ // Non-admin agent: only its own integrations.
53
+ const record = await super.get(target);
54
+ if (!record)
55
+ return NOT_FOUND();
56
+ if (record.agentId !== auth.agentId)
57
+ return NOT_FOUND();
58
+ return record;
59
+ }
15
60
  async search(query) {
16
61
  const auth = await this._auth();
17
62
  if (auth.kind === "anonymous")
@@ -66,7 +111,12 @@ export class Integration extends databases.flair.Integration {
66
111
  if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
67
112
  return super.delete(id);
68
113
  }
69
- const record = await this.get(id);
114
+ // Use super.get(id), NOT this.get(id): the new get() override above 404s
115
+ // (a truthy Response) for a non-owner id, which would otherwise defeat
116
+ // the `if (!record)` check below and mis-route a genuinely-missing
117
+ // record into the FORBIDDEN branch instead of a clean super.delete(id)
118
+ // no-op. Mirrors Memory.ts's delete() — same rationale, same fix.
119
+ const record = await super.get(id);
70
120
  if (!record)
71
121
  return super.delete(id);
72
122
  if (record.agentId !== auth.agentId) {