44// The host is the real thing: hub routes, the hub request logger and the hub
55// session middleware are all live, and the artifact principal is resolved out
66// of the hub's own request context (`c.var.user`) rather than a local variable.
7- // The three seams are implemented against the host's OWN control plane
8- // (interchange `principal` / `user` rows), which is the point: the module knows
9- // nothing about them, the host supplies them.
7+ // The two seams (authz + provenance) are implemented against the host's OWN
8+ // control plane (interchange `principal` / `user` rows), which is the point:
9+ // the module knows nothing about them, the host supplies them.
1010//
1111// This module only BUILDS the host. The acceptance scenarios live in
1212// `test/acceptance.test.ts` and run under `bun test`, so they are collected by
1313// CI like any other test instead of being a hand-rolled assert script nothing
1414// executes.
15- import { and , eq , inArray , sql } from "drizzle-orm" ;
15+ import { and , eq , sql } from "drizzle-orm" ;
1616import { Hono } from "hono" ;
1717import type { Context } from "hono" ;
1818import { createApp , type AppEnv } from "@intx/hub-api" ;
@@ -30,10 +30,10 @@ import {
3030 InlineContentStore ,
3131 mountArtifacts ,
3232 runArtifactMigrations ,
33+ type AdminAuthz ,
3334 type ArtifactDb ,
3435 type ResolvedPrincipal ,
3536 type ContentStore ,
36- type Identity ,
3737 type Provenance ,
3838} from "@corbits/artifact-core" ;
3939
@@ -54,76 +54,67 @@ function parsePostgresUrl(raw: string) {
5454 } ;
5555}
5656
57- /** Seam B, implemented against the host's own directory tables. */
58- function createIdentity ( db : ArtifactDb ) : Identity {
59- return {
60- async ownerNames ( tenantId , ownerPrincipalIds ) {
61- const principals = await db
62- . select ( { id : intxSchema . principal . id , refId : intxSchema . principal . refId } )
63- . from ( intxSchema . principal )
64- . where (
65- and (
66- eq ( intxSchema . principal . tenantId , tenantId ) ,
67- inArray ( intxSchema . principal . id , ownerPrincipalIds ) ,
68- ) ,
69- ) ;
70- const refIds = [ ...new Set ( principals . map ( ( p ) => p . refId ) ) ] ;
71- const users =
72- refIds . length > 0
73- ? await db
74- . select ( { id : intxSchema . user . id , name : intxSchema . user . name } )
75- . from ( intxSchema . user )
76- . where ( inArray ( intxSchema . user . id , refIds ) )
77- : [ ] ;
78- const nameByRefId = new Map ( users . map ( ( u ) => [ u . id , u . name ] ) ) ;
79- return new Map ( principals . map ( ( p ) => [ p . id , nameByRefId . get ( p . refId ) ?? null ] ) ) ;
80- } ,
81-
82- async ownerMemberPrincipalId ( scope ) {
83- // An agent principal's refId names the human who owns it in this host.
84- const [ agent ] = await db
85- . select ( { refId : intxSchema . principal . refId } )
86- . from ( intxSchema . principal )
87- . where (
88- and (
89- eq ( intxSchema . principal . id , scope . principal ) ,
90- eq ( intxSchema . principal . tenantId , scope . tenant ) ,
91- eq ( intxSchema . principal . kind , "agent" ) ,
92- ) ,
93- )
94- . limit ( 1 ) ;
95- if ( ! agent ) return null ;
96- const [ member ] = await db
97- . select ( { id : intxSchema . principal . id } )
98- . from ( intxSchema . principal )
99- . where (
100- and (
101- eq ( intxSchema . principal . tenantId , scope . tenant ) ,
102- eq ( intxSchema . principal . kind , "user" ) ,
103- eq ( intxSchema . principal . refId , agent . refId ) ,
104- eq ( intxSchema . principal . status , "active" ) ,
105- ) ,
106- )
107- . limit ( 1 ) ;
108- return member ?. id ?? null ;
109- } ,
57+ /**
58+ * Resolve the human member who owns an agent principal — the agent's `refId`
59+ * names them in this host's own `principal` table. Used only by
60+ * `canAdminister`, which is called after the exact-owner match already
61+ * failed, so an unowned or non-agent `ownerPrincipalId` correctly resolves to
62+ * "nobody".
63+ */
64+ async function ownerMemberPrincipalId (
65+ db : ArtifactDb ,
66+ tenantId : string ,
67+ agentPrincipalId : string ,
68+ ) : Promise < string | null > {
69+ const [ agent ] = await db
70+ . select ( { refId : intxSchema . principal . refId } )
71+ . from ( intxSchema . principal )
72+ . where (
73+ and (
74+ eq ( intxSchema . principal . id , agentPrincipalId ) ,
75+ eq ( intxSchema . principal . tenantId , tenantId ) ,
76+ eq ( intxSchema . principal . kind , "agent" ) ,
77+ ) ,
78+ )
79+ . limit ( 1 ) ;
80+ if ( ! agent ) return null ;
81+ const [ member ] = await db
82+ . select ( { id : intxSchema . principal . id } )
83+ . from ( intxSchema . principal )
84+ . where (
85+ and (
86+ eq ( intxSchema . principal . tenantId , tenantId ) ,
87+ eq ( intxSchema . principal . kind , "user" ) ,
88+ eq ( intxSchema . principal . refId , agent . refId ) ,
89+ eq ( intxSchema . principal . status , "active" ) ,
90+ ) ,
91+ )
92+ . limit ( 1 ) ;
93+ return member ?. id ?? null ;
94+ }
11095
111- async principalIdsByKind ( tenantId , kind ) {
112- const rows = await db
113- . select ( { id : intxSchema . principal . id } )
114- . from ( intxSchema . principal )
115- . where (
116- and (
117- eq ( intxSchema . principal . tenantId , tenantId ) ,
118- eq ( intxSchema . principal . kind , kind ) ,
119- ) ,
120- ) ;
121- return rows . map ( ( r ) => r . id ) ;
96+ /**
97+ * Seam A, implemented honestly against the host's own control plane.
98+ * `isAdmin` is the host's own coarse admin check; `canAdminister` folds it
99+ * together with "the member who owns the producing agent may administer its
100+ * artifact" — the core calls this only after the exact-owner match already
101+ * failed, so neither branch needs to re-check ownership.
102+ */
103+ function createAdminAuthz ( db : ArtifactDb , isAdmin : ( ) => Promise < boolean > ) : AdminAuthz {
104+ return {
105+ async canAdminister ( scope , row ) {
106+ if ( await isAdmin ( ) ) return true ;
107+ if ( row . ownerPrincipalId === null ) return false ;
108+ const member = await ownerMemberPrincipalId (
109+ db ,
110+ scope . tenant ,
111+ row . ownerPrincipalId ,
112+ ) ;
113+ return member !== null && member === scope . principal ;
122114 } ,
123-
124115 // This host has exactly one tenant, so a cross-tenant read is always
125116 // refused. A multi-tenant host would check active membership there.
126- async ownerIsMemberOfTenant ( ) {
117+ async canReadTenant ( ) {
127118 return false ;
128119 } ,
129120 } ;
@@ -308,8 +299,6 @@ export async function createReferenceHost(): Promise<ReferenceHost> {
308299 endSession : refuse ( "endSession" ) ,
309300 } ;
310301
311- const identity = createIdentity ( db ) ;
312-
313302 /** Build a host app with one ContentStore backend mounted. */
314303 function buildApp ( contentStore : ContentStore , isAdmin : ( ) => Promise < boolean > ) {
315304 const app = createApp ( {
@@ -333,8 +322,7 @@ export async function createReferenceHost(): Promise<ReferenceHost> {
333322 db,
334323 contentStore,
335324 resolvePrincipal,
336- adminAuthz : { isAdmin } ,
337- identity,
325+ adminAuthz : createAdminAuthz ( db , isAdmin ) ,
338326 provenance,
339327 } ) ;
340328 const mounted = app . route ( "/api" , api ) ;
0 commit comments