@tpsdev-ai/flair 0.10.1 → 0.12.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.
Files changed (37) hide show
  1. package/dist/cli.js +318 -34
  2. package/dist/resources/Agent.js +20 -7
  3. package/dist/resources/AgentCard.js +6 -0
  4. package/dist/resources/AgentSeed.js +7 -1
  5. package/dist/resources/Credential.js +24 -13
  6. package/dist/resources/IngestEvents.js +7 -0
  7. package/dist/resources/Instance.js +13 -0
  8. package/dist/resources/Integration.js +64 -0
  9. package/dist/resources/Memory.js +53 -8
  10. package/dist/resources/MemoryBootstrap.js +8 -0
  11. package/dist/resources/MemoryConsolidate.js +7 -1
  12. package/dist/resources/MemoryFeed.js +8 -0
  13. package/dist/resources/MemoryGrant.js +79 -0
  14. package/dist/resources/MemoryMaintenance.js +1 -1
  15. package/dist/resources/MemoryReflect.js +7 -1
  16. package/dist/resources/MemoryReindex.js +7 -1
  17. package/dist/resources/OAuthClient.js +15 -0
  18. package/dist/resources/ObsAgentSnapshot.js +13 -0
  19. package/dist/resources/ObsEventFeed.js +13 -0
  20. package/dist/resources/ObsOffice.js +19 -0
  21. package/dist/resources/OrgEvent.js +37 -21
  22. package/dist/resources/OrgEventCatchup.js +8 -0
  23. package/dist/resources/OrgEventMaintenance.js +7 -0
  24. package/dist/resources/PairingToken.js +14 -0
  25. package/dist/resources/Peer.js +15 -0
  26. package/dist/resources/Presence.js +300 -0
  27. package/dist/resources/Relationship.js +13 -7
  28. package/dist/resources/SemanticSearch.js +25 -8
  29. package/dist/resources/Soul.js +18 -9
  30. package/dist/resources/SoulFeed.js +7 -0
  31. package/dist/resources/WorkspaceLatest.js +7 -0
  32. package/dist/resources/WorkspaceState.js +38 -28
  33. package/dist/resources/XAA.js +8 -0
  34. package/dist/resources/agent-auth.js +209 -0
  35. package/dist/resources/auth-middleware.js +89 -57
  36. package/package.json +1 -1
  37. package/schemas/schema.graphql +9 -0
@@ -1,6 +1,6 @@
1
1
  import { databases } from "@harperfast/harper";
2
2
  import { patchRecord, withDetachedTxn } from "./table-helpers.js";
3
- import { isAdmin } from "./auth-middleware.js";
3
+ import { isAdmin, resolveAgentAuth } from "./agent-auth.js";
4
4
  import { getEmbedding, getModelId } from "./embeddings-provider.js";
5
5
  import { scanFields, isStrictMode } from "./content-safety.js";
6
6
  import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
@@ -16,16 +16,22 @@ export class Memory extends databases.flair.Memory {
16
16
  * Non-admin calls also check MemoryGrant to include granted memories.
17
17
  */
18
18
  async search(query) {
19
- // Access request context via Harper's Resource instance context
19
+ // Access request context via Harper's Resource instance context.
20
20
  const ctx = this.getContext?.();
21
- const request = ctx?.request ?? ctx;
22
- const authAgent = request?.tpsAgent;
23
- const isAdminAgent = request?.tpsAgentIsAdmin ?? false;
24
- // No auth context (internal admin call) or admin agent — unfiltered
25
- if (!authAgent || isAdminAgent) {
21
+ const auth = await resolveAgentAuth(ctx);
22
+ // Anonymous HTTP must NOT read memories. (Previously `!authAgent` was treated
23
+ // as unfiltered the anonymous-read leak once the gate stops rejecting.)
24
+ if (auth.kind === "anonymous") {
25
+ return new Response(JSON.stringify({ error: "authentication required" }), {
26
+ status: 401, headers: { "content-type": "application/json" },
27
+ });
28
+ }
29
+ // Trusted internal call (no request context) or admin agent — unfiltered.
30
+ if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
26
31
  return super.search(query);
27
32
  }
28
- // Collect agentIds this agent may read: own + any granted owners
33
+ // Non-admin agent: scope to own + granted owners.
34
+ const authAgent = auth.agentId;
29
35
  const allowedOwners = [authAgent];
30
36
  try {
31
37
  for await (const grant of databases.flair.MemoryGrant.search({
@@ -68,6 +74,26 @@ export class Memory extends databases.flair.Memory {
68
74
  if (!rl.allowed)
69
75
  return rateLimitResponse(rl.retryAfterMs, "write");
70
76
  }
77
+ // Create ownership: a non-admin agent may only write memories it owns. Use
78
+ // resolveAgentAuth (reads the gate's tpsAgent annotation) — NOT context.user
79
+ // .username, which is the fallback "admin" super_user while de-elevation is
80
+ // dormant and would wrongly 403 every agent's own write. internal/admin → pass.
81
+ {
82
+ const auth = await resolveAgentAuth(ctx);
83
+ // Anonymous HTTP must NOT write. Pre-flip the global gate rejected no-auth
84
+ // upstream; with the non-rejecting gate, each write path self-enforces (same
85
+ // rule search() applies to reads).
86
+ if (auth.kind === "anonymous") {
87
+ return new Response(JSON.stringify({ error: "authentication required" }), {
88
+ status: 401, headers: { "Content-Type": "application/json" },
89
+ });
90
+ }
91
+ if (auth.kind === "agent" && !auth.isAdmin && content?.agentId && content.agentId !== auth.agentId) {
92
+ return new Response(JSON.stringify({ error: "forbidden: cannot write memory owned by another agent" }), {
93
+ status: 403, headers: { "Content-Type": "application/json" },
94
+ });
95
+ }
96
+ }
71
97
  content.durability ||= "standard";
72
98
  content.createdAt = new Date().toISOString();
73
99
  content.updatedAt = content.createdAt;
@@ -151,6 +177,25 @@ export class Memory extends databases.flair.Memory {
151
177
  delete content._reindex;
152
178
  return super.put(content);
153
179
  }
180
+ // Create/update ownership (same rule as post): a non-admin agent may only
181
+ // write memories it owns, via resolveAgentAuth (gate annotation), not
182
+ // context.user.username (the dormant-de-elevation fallback is "admin").
183
+ // The _reindex admin path above bypasses this.
184
+ {
185
+ const octx = this.getContext?.();
186
+ const auth = await resolveAgentAuth(octx);
187
+ // Anonymous HTTP must NOT write (non-rejecting gate → self-enforce here).
188
+ if (auth.kind === "anonymous") {
189
+ return new Response(JSON.stringify({ error: "authentication required" }), {
190
+ status: 401, headers: { "Content-Type": "application/json" },
191
+ });
192
+ }
193
+ if (auth.kind === "agent" && !auth.isAdmin && content?.agentId && content.agentId !== auth.agentId) {
194
+ return new Response(JSON.stringify({ error: "forbidden: cannot write memory owned by another agent" }), {
195
+ status: 403, headers: { "Content-Type": "application/json" },
196
+ });
197
+ }
198
+ }
154
199
  const now = new Date().toISOString();
155
200
  content.updatedAt = now;
156
201
  // Set defaults that post() sets — put() is also used for new records via CLI
@@ -1,4 +1,5 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
+ import { allowVerified } from "./agent-auth.js";
2
3
  import { getEmbedding } from "./embeddings-provider.js";
3
4
  import { wrapUntrusted } from "./content-safety.js";
4
5
  /**
@@ -40,6 +41,13 @@ function formatMemory(m, supersedes) {
40
41
  return base;
41
42
  }
42
43
  export class BootstrapMemories extends Resource {
44
+ // Self-authorize via the Ed25519 agent verify (the auth reshape removes the
45
+ // gate's admin super_user elevation, so custom resources must self-gate or
46
+ // Harper denies them for the least-privilege flair_agent role). Any verified
47
+ // agent may bootstrap; per-agent scoping is enforced in post() below.
48
+ async allowCreate() {
49
+ return allowVerified(this.getContext?.());
50
+ }
43
51
  async post(data, _context) {
44
52
  const { agentId: bodyAgentId, currentTask, maxTokens = 4000, includeSoul = true, since, channel, // e.g., "discord", "tps-mail", "claude-code"
45
53
  surface, // e.g., "tps-build", "tps-review", "cli-session"
@@ -16,7 +16,7 @@
16
16
  * prompt string
17
17
  */
18
18
  import { Resource, databases } from "@harperfast/harper";
19
- import { isAdmin } from "./auth-middleware.js";
19
+ import { isAdmin, allowVerified } from "./agent-auth.js";
20
20
  function parseDuration(s) {
21
21
  const m = s.match(/^(\d+)([dhm])$/);
22
22
  if (!m)
@@ -56,6 +56,12 @@ function evaluate(record, now, olderThanMs) {
56
56
  return { memory, suggestion: "keep", reason: `Retrieved ${count} times, ${Math.round(daysSinceRetrieved)} days since last retrieval` };
57
57
  }
58
58
  export class ConsolidateMemories extends Resource {
59
+ // Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
60
+ // admin elevation). Any verified agent may consolidate; the isAdmin checks in
61
+ // post() handle finer-grained authorization.
62
+ async allowCreate() {
63
+ return allowVerified(this.getContext?.());
64
+ }
59
65
  async post(data) {
60
66
  const { agentId: bodyAgentId, scope = "persistent", olderThan = "30d", limit = 20 } = data || {};
61
67
  // See SemanticSearch / MemoryBootstrap — `this.request` isn't populated on
@@ -1,6 +1,14 @@
1
1
  import { Resource, databases } from "@harperfast/harper";
2
+ import { allowVerified } from "./agent-auth.js";
2
3
  import { computeContentHash, findExistingMemoryByContentHash } from "./memory-feed-lib.js";
3
4
  export class FeedMemories extends Resource {
5
+ // Self-authorize via the Ed25519 agent verify (the auth reshape removes the
6
+ // gate's admin elevation). NOTE: post() trusts content.agentId from the body —
7
+ // closing that create-spoofing gap is tracked with the table-resource
8
+ // create-ownership work (Memory.allowCreate), not in this auth-coverage pass.
9
+ async allowCreate() {
10
+ return allowVerified(this.getContext?.());
11
+ }
4
12
  async post(content) {
5
13
  const agentId = String(content?.agentId ?? "");
6
14
  const body = String(content?.content ?? "");
@@ -0,0 +1,79 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { resolveAgentAuth } from "./agent-auth.js";
3
+ const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
4
+ const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
5
+ /**
6
+ * MemoryGrant — an agent (ownerId) grants another (granteeId) scoped access to its
7
+ * memories. Self-authorizes now that the global gate is non-rejecting (previously a
8
+ * pure @table protected only by the gate → anonymous could read/write any grant).
9
+ *
10
+ * Write: non-admin agents may only create/modify/delete grants they OWN
11
+ * (ownerId === self) — you can only share your own memories.
12
+ * Read: non-admin agents see grants where they are owner OR grantee.
13
+ * Internal calls (e.g. Memory.search's grant lookup) and admins pass unfiltered.
14
+ */
15
+ export class MemoryGrant extends databases.flair.MemoryGrant {
16
+ _auth() {
17
+ return resolveAgentAuth(this.getContext?.());
18
+ }
19
+ async search(query) {
20
+ const auth = await this._auth();
21
+ if (auth.kind === "anonymous")
22
+ return UNAUTH();
23
+ if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
24
+ return super.search(query);
25
+ }
26
+ // owner OR grantee
27
+ const scope = {
28
+ operator: "or",
29
+ conditions: [
30
+ { attribute: "ownerId", comparator: "equals", value: auth.agentId },
31
+ { attribute: "granteeId", comparator: "equals", value: auth.agentId },
32
+ ],
33
+ };
34
+ if (query && typeof query === "object" && !Array.isArray(query)) {
35
+ const existing = query.conditions ?? [];
36
+ query.conditions = Array.isArray(existing) ? [scope, ...existing] : [scope, existing];
37
+ return super.search(query);
38
+ }
39
+ const conditions = Array.isArray(query) && query.length > 0 ? [scope, ...query] : [scope];
40
+ return super.search(conditions);
41
+ }
42
+ async post(content, context) {
43
+ const denied = await this._enforceOwnerWrite(content);
44
+ if (denied)
45
+ return denied;
46
+ content.createdAt ||= new Date().toISOString();
47
+ return super.post(content, context);
48
+ }
49
+ async put(content, context) {
50
+ const denied = await this._enforceOwnerWrite(content);
51
+ if (denied)
52
+ return denied;
53
+ return super.put(content, context);
54
+ }
55
+ async delete(id, context) {
56
+ const auth = await this._auth();
57
+ if (auth.kind === "anonymous")
58
+ return UNAUTH();
59
+ if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
60
+ return super.delete(id, context);
61
+ }
62
+ const record = await this.get(id);
63
+ if (!record)
64
+ return super.delete(id, context);
65
+ if (record.ownerId !== auth.agentId) {
66
+ return FORBIDDEN("forbidden: cannot delete a grant owned by another agent");
67
+ }
68
+ return super.delete(id, context);
69
+ }
70
+ async _enforceOwnerWrite(content) {
71
+ const auth = await this._auth();
72
+ if (auth.kind === "anonymous")
73
+ return UNAUTH();
74
+ if (auth.kind === "agent" && !auth.isAdmin && content?.ownerId && content.ownerId !== auth.agentId) {
75
+ return FORBIDDEN("forbidden: cannot grant access to another agent's memories");
76
+ }
77
+ return null;
78
+ }
79
+ }
@@ -17,7 +17,7 @@
17
17
  * to gate auth correctly.
18
18
  */
19
19
  import { Resource, databases } from "@harperfast/harper";
20
- import { isAdmin } from "./auth-middleware.js";
20
+ import { isAdmin } from "./agent-auth.js";
21
21
  export class MemoryMaintenance extends Resource {
22
22
  /** POST requires auth — either an agent acting on its own memories, or admin. */
23
23
  allowCreate() {
@@ -20,7 +20,7 @@
20
20
  * count number — number of memories included
21
21
  */
22
22
  import { Resource, databases } from "@harperfast/harper";
23
- import { isAdmin } from "./auth-middleware.js";
23
+ import { isAdmin, allowVerified } from "./agent-auth.js";
24
24
  import { patchRecordSilent } from "./table-helpers.js";
25
25
  const FOCUS_PROMPTS = {
26
26
  lessons_learned: "Review these memories and identify concrete lessons learned. For each lesson: what happened, what you learned, and how it should change future behavior. Write atomic memories with durability=persistent.",
@@ -29,6 +29,12 @@ const FOCUS_PROMPTS = {
29
29
  errors: "Extract errors, bugs, and failures. For each: what failed, root cause, and fix applied. These are high-value persistent memories.",
30
30
  };
31
31
  export class ReflectMemories extends Resource {
32
+ // Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
33
+ // admin elevation). Any verified agent may reflect; the isAdmin checks in post()
34
+ // handle finer-grained authorization.
35
+ async allowCreate() {
36
+ return allowVerified(this.getContext?.());
37
+ }
32
38
  async post(data) {
33
39
  const { agentId: bodyAgentId, scope = "recent", since, maxMemories = 50, focus = "lessons_learned", tag, } = data || {};
34
40
  // Authenticated identity comes from getContext().request, not this.request
@@ -24,8 +24,14 @@
24
24
  * This endpoint is idempotent: running it again finds zero drift.
25
25
  */
26
26
  import { Resource, databases } from "@harperfast/harper";
27
- import { isAdmin } from "./auth-middleware.js";
27
+ import { isAdmin, allowAdmin } from "./agent-auth.js";
28
28
  export class MemoryReindex extends Resource {
29
+ // Admin-only: permit verified ADMIN agents (Basic-admin is super_user and
30
+ // bypasses allow*); non-admin agents denied. Mirrors the handler's admin gate
31
+ // but is the real authorization now that the gate no longer elevates agents.
32
+ async allowCreate() {
33
+ return allowAdmin(this.getContext?.());
34
+ }
29
35
  async post(data) {
30
36
  const ctx = this.getContext?.();
31
37
  const request = ctx?.request ?? ctx ?? this.request;
@@ -0,0 +1,15 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * OAuthClient holds registered OAuth client records (incl. secrets) — admin/system
5
+ * only, no agent grant. OAuth endpoints (OAuthRegister/Authorize/Token) read+write
6
+ * clients via internal calls (allowAdmin permits internal), so the OAuth flows keep
7
+ * working; direct anonymous table access is denied. Self-authorizes now that the
8
+ * global gate is non-rejecting.
9
+ */
10
+ export class OAuthClient extends databases.flair.OAuthClient {
11
+ allowRead() { return allowAdmin(this.getContext?.()); }
12
+ allowCreate() { return allowAdmin(this.getContext?.()); }
13
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
14
+ allowDelete() { return allowAdmin(this.getContext?.()); }
15
+ }
@@ -0,0 +1,13 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowVerified, allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * ObsAgentSnapshot — observatory per-agent snapshot read-model. Writes are
5
+ * system-driven (internal sync); agents may read. See ObsOffice for the
6
+ * allowRead=allowVerified security default (flag for Sherlock).
7
+ */
8
+ export class ObsAgentSnapshot extends databases.flair.ObsAgentSnapshot {
9
+ allowRead() { return allowVerified(this.getContext?.()); }
10
+ allowCreate() { return allowAdmin(this.getContext?.()); }
11
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
12
+ allowDelete() { return allowAdmin(this.getContext?.()); }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowVerified, allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * ObsEventFeed — observatory event-feed read-model. Writes are system-driven
5
+ * (internal sync); agents may read. See ObsOffice for the allowRead=allowVerified
6
+ * security default (flag for Sherlock).
7
+ */
8
+ export class ObsEventFeed extends databases.flair.ObsEventFeed {
9
+ allowRead() { return allowVerified(this.getContext?.()); }
10
+ allowCreate() { return allowAdmin(this.getContext?.()); }
11
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
12
+ allowDelete() { return allowAdmin(this.getContext?.()); }
13
+ }
@@ -0,0 +1,19 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowVerified, allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * ObsOffice — observatory office-layout read-model. Writes are system-driven
5
+ * (internal sync); agents may read. Self-authorizes now that the global gate is
6
+ * non-rejecting.
7
+ *
8
+ * SECURITY DEFAULT (flag for Sherlock): allowRead is allowVerified (anonymous
9
+ * denied). The ObservationCenter HTML shell is public but its data API calls
10
+ * authenticate (admin-pass). If the public Office Space renderer needs anonymous
11
+ * Obs reads, relax to allowRead=true WITH field-allowlisting (as Presence.get does)
12
+ * — do NOT expose raw rows publicly.
13
+ */
14
+ export class ObsOffice extends databases.flair.ObsOffice {
15
+ allowRead() { return allowVerified(this.getContext?.()); }
16
+ allowCreate() { return allowAdmin(this.getContext?.()); }
17
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
18
+ allowDelete() { return allowAdmin(this.getContext?.()); }
19
+ }
@@ -1,42 +1,58 @@
1
1
  /**
2
2
  * OrgEvent.ts — Harper table resource for org-wide activity events.
3
3
  *
4
- * Auth: Ed25519 middleware sets request.tpsAgent.
5
- * Write: authorId must match authenticated agent (or admin).
6
- * Read: any authenticated participant can read (org-scoped).
4
+ * Auth (self-enforced now that the global gate is non-rejecting):
5
+ * Read — any verified agent/admin (org-scoped); anonymous denied.
6
+ * Write authorId must match the authenticated agent (or admin); anonymous denied.
7
+ *
8
+ * The previous version read context.request.tpsAgent and treated a MISSING agent
9
+ * as trusted (`if (agentId && …)` / `if (!agentId) super.delete`), so anonymous
10
+ * requests slipped through once the gate stopped rejecting. resolveAgentAuth
11
+ * distinguishes internal/agent/anonymous explicitly.
7
12
  */
8
13
  import { databases } from "@harperfast/harper";
14
+ import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
15
+ const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
16
+ const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
9
17
  export class OrgEvent extends databases.flair.OrgEvent {
10
- async post(content, context) {
11
- const agentId = context?.request?.tpsAgent;
12
- // authorId must match authenticated agent (unless admin)
13
- if (agentId && !context?.request?.tpsAgentIsAdmin && content.authorId !== agentId) {
14
- return new Response(JSON.stringify({ error: "forbidden: authorId must match authenticated agent" }), { status: 403, headers: { "Content-Type": "application/json" } });
15
- }
16
- // Generate composite ID if not provided
17
- if (!content.id) {
18
- content.id = `${content.authorId}-${Date.now()}`;
18
+ allowRead() { return allowVerified(this.getContext?.()); }
19
+ _auth() {
20
+ return resolveAgentAuth(this.getContext?.());
21
+ }
22
+ async post(content) {
23
+ const auth = await this._auth();
24
+ if (auth.kind === "anonymous")
25
+ return UNAUTH();
26
+ if (auth.kind === "agent" && !auth.isAdmin && content.authorId !== auth.agentId) {
27
+ return FORBIDDEN("forbidden: authorId must match authenticated agent");
19
28
  }
29
+ if (!content.id)
30
+ content.id = `${content.authorId}-${new Date().toISOString()}`;
20
31
  content.createdAt = new Date().toISOString();
21
- // Harper 5: table resources use put() for create/upsert (post() removed)
32
+ // Harper 5: table resources use put() for create/upsert (post() removed).
22
33
  return databases.flair.OrgEvent.put(content);
23
34
  }
24
- async put(content, context) {
25
- const agentId = context?.request?.tpsAgent;
26
- if (agentId && !context?.request?.tpsAgentIsAdmin && content.authorId !== agentId) {
27
- return new Response(JSON.stringify({ error: "forbidden: authorId must match authenticated agent" }), { status: 403, headers: { "Content-Type": "application/json" } });
35
+ async put(content) {
36
+ const auth = await this._auth();
37
+ if (auth.kind === "anonymous")
38
+ return UNAUTH();
39
+ if (auth.kind === "agent" && !auth.isAdmin && content.authorId !== auth.agentId) {
40
+ return FORBIDDEN("forbidden: authorId must match authenticated agent");
28
41
  }
29
42
  return databases.flair.OrgEvent.put(content);
30
43
  }
31
44
  async delete(id, context) {
32
- const agentId = context?.request?.tpsAgent;
33
- if (!agentId)
45
+ const auth = await this._auth();
46
+ if (auth.kind === "anonymous")
47
+ return UNAUTH();
48
+ if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
34
49
  return super.delete(id, context);
50
+ }
35
51
  const record = await this.get(id);
36
52
  if (!record)
37
53
  return super.delete(id, context);
38
- if (!context?.request?.tpsAgentIsAdmin && record.authorId !== agentId) {
39
- return new Response(JSON.stringify({ error: "forbidden: cannot delete events authored by another agent" }), { status: 403, headers: { "Content-Type": "application/json" } });
54
+ if (record.authorId !== auth.agentId) {
55
+ return FORBIDDEN("forbidden: cannot delete events authored by another agent");
40
56
  }
41
57
  return super.delete(id, context);
42
58
  }
@@ -10,7 +10,15 @@
10
10
  * Limit 50 events max.
11
11
  */
12
12
  import { Resource, databases } from "@harperfast/harper";
13
+ import { allowVerified } from "./agent-auth.js";
13
14
  export class OrgEventCatchup extends Resource {
15
+ // Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
16
+ // admin elevation). Any verified agent may catch up; participant scoping is in
17
+ // get(). Uses getContext().request — the reliable v5 path (this.request is not
18
+ // populated on Harper v5 Resources).
19
+ async allowRead() {
20
+ return allowVerified(this.getContext?.());
21
+ }
14
22
  // HarperDB calls get(pathInfo, context) where pathInfo is the URL segment after /OrgEventCatchup/
15
23
  async get(pathInfo) {
16
24
  const request = this.request;
@@ -5,7 +5,14 @@
5
5
  * Auth: admin only.
6
6
  */
7
7
  import { Resource, databases } from "@harperfast/harper";
8
+ import { allowAdmin } from "./agent-auth.js";
8
9
  export class OrgEventMaintenance extends Resource {
10
+ // Admin-only: permit verified ADMIN agents (Basic-admin is super_user and
11
+ // bypasses allow*); non-admin agents denied. Real authorization now that the
12
+ // gate no longer elevates agents to admin.
13
+ async allowCreate() {
14
+ return allowAdmin(this.getContext?.());
15
+ }
9
16
  async post(_data, context) {
10
17
  // Admin-only
11
18
  if (!context?.request?.tpsAgentIsAdmin) {
@@ -0,0 +1,14 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * PairingToken holds one-time federation pairing secrets — admin/system only,
5
+ * no agent grant. The FederationPair endpoint consumes tokens via internal calls
6
+ * (allowAdmin permits internal), so dedicated flows keep working; direct anonymous
7
+ * table access is denied. Self-authorizes now that the global gate is non-rejecting.
8
+ */
9
+ export class PairingToken extends databases.flair.PairingToken {
10
+ allowRead() { return allowAdmin(this.getContext?.()); }
11
+ allowCreate() { return allowAdmin(this.getContext?.()); }
12
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
13
+ allowDelete() { return allowAdmin(this.getContext?.()); }
14
+ }
@@ -0,0 +1,15 @@
1
+ import { databases } from "@harperfast/harper";
2
+ import { allowAdmin } from "./agent-auth.js";
3
+ /**
4
+ * Peer holds federation peer records (URLs + credentials) — system/admin data,
5
+ * no agent grant. Before the auth flip the global gate was its only protection;
6
+ * the non-rejecting gate means it must self-authorize. Admin/internal only on
7
+ * every path. (Federation pairing/sync use dedicated endpoints — FederationPair,
8
+ * FederationSync — not direct Peer table access.)
9
+ */
10
+ export class Peer extends databases.flair.Peer {
11
+ allowRead() { return allowAdmin(this.getContext?.()); }
12
+ allowCreate() { return allowAdmin(this.getContext?.()); }
13
+ allowUpdate() { return allowAdmin(this.getContext?.()); }
14
+ allowDelete() { return allowAdmin(this.getContext?.()); }
15
+ }