@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.
- package/dist/cli.js +318 -34
- package/dist/resources/Agent.js +20 -7
- package/dist/resources/AgentCard.js +6 -0
- package/dist/resources/AgentSeed.js +7 -1
- package/dist/resources/Credential.js +24 -13
- package/dist/resources/IngestEvents.js +7 -0
- package/dist/resources/Instance.js +13 -0
- package/dist/resources/Integration.js +64 -0
- package/dist/resources/Memory.js +53 -8
- package/dist/resources/MemoryBootstrap.js +8 -0
- package/dist/resources/MemoryConsolidate.js +7 -1
- package/dist/resources/MemoryFeed.js +8 -0
- package/dist/resources/MemoryGrant.js +79 -0
- package/dist/resources/MemoryMaintenance.js +1 -1
- package/dist/resources/MemoryReflect.js +7 -1
- package/dist/resources/MemoryReindex.js +7 -1
- package/dist/resources/OAuthClient.js +15 -0
- package/dist/resources/ObsAgentSnapshot.js +13 -0
- package/dist/resources/ObsEventFeed.js +13 -0
- package/dist/resources/ObsOffice.js +19 -0
- package/dist/resources/OrgEvent.js +37 -21
- package/dist/resources/OrgEventCatchup.js +8 -0
- package/dist/resources/OrgEventMaintenance.js +7 -0
- package/dist/resources/PairingToken.js +14 -0
- package/dist/resources/Peer.js +15 -0
- package/dist/resources/Presence.js +300 -0
- package/dist/resources/Relationship.js +13 -7
- package/dist/resources/SemanticSearch.js +25 -8
- package/dist/resources/Soul.js +18 -9
- package/dist/resources/SoulFeed.js +7 -0
- package/dist/resources/WorkspaceLatest.js +7 -0
- package/dist/resources/WorkspaceState.js +38 -28
- package/dist/resources/XAA.js +8 -0
- package/dist/resources/agent-auth.js +209 -0
- package/dist/resources/auth-middleware.js +89 -57
- package/package.json +1 -1
- package/schemas/schema.graphql +9 -0
package/dist/resources/Memory.js
CHANGED
|
@@ -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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
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
|
|
33
|
-
if (
|
|
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 (
|
|
39
|
-
return
|
|
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
|
+
}
|