feat(core): private-plugin SDK, PluginVisibility enum, and Go 1.26.4 bump

Add private-plugin RPCs (ListPrivatePlugins, DeletePrivatePlugin,
DeletePrivatePluginVersion, ListPrivatePluginInstallSites) and
ListMyAccounts to the proto/generated stubs; introduce PluginVisibility
enum replacing the loose string field; add ModPlugin.Private + Coords()
routing to @private/<name>@<version>; update ninja CLI to use
VisibilityLabel helper; bump go directive to 1.26.4 for ABI alignment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-04 01:00:50 +08:00
parent 06cabd6eb9
commit 264116f44e
7 changed files with 1445 additions and 247 deletions

View File

@ -483,7 +483,7 @@ func newPluginStatusCmd() *cobra.Command {
continue continue
} }
for _, p := range gs.Msg.Plugins { for _, p := range gs.Msg.Plugins {
fmt.Printf(" @%s/%s [%s]\n", s.Slug, p.Name, p.Visibility) fmt.Printf(" @%s/%s [%s]\n", s.Slug, p.Name, core.VisibilityLabel(p.Visibility))
} }
} }
return nil return nil

2
go.mod
View File

@ -1,6 +1,6 @@
module git.dev.alexdunmow.com/block/core module git.dev.alexdunmow.com/block/core
go 1.26 go 1.26.4
require ( require (
connectrpc.com/connect v1.20.0 connectrpc.com/connect v1.20.0

View File

@ -66,6 +66,18 @@ const (
// PluginRegistryServiceListCategoriesProcedure is the fully-qualified name of the // PluginRegistryServiceListCategoriesProcedure is the fully-qualified name of the
// PluginRegistryService's ListCategories RPC. // PluginRegistryService's ListCategories RPC.
PluginRegistryServiceListCategoriesProcedure = "/orchestrator.v1.PluginRegistryService/ListCategories" PluginRegistryServiceListCategoriesProcedure = "/orchestrator.v1.PluginRegistryService/ListCategories"
// PluginRegistryServiceListPrivatePluginsProcedure is the fully-qualified name of the
// PluginRegistryService's ListPrivatePlugins RPC.
PluginRegistryServiceListPrivatePluginsProcedure = "/orchestrator.v1.PluginRegistryService/ListPrivatePlugins"
// PluginRegistryServiceDeletePrivatePluginProcedure is the fully-qualified name of the
// PluginRegistryService's DeletePrivatePlugin RPC.
PluginRegistryServiceDeletePrivatePluginProcedure = "/orchestrator.v1.PluginRegistryService/DeletePrivatePlugin"
// PluginRegistryServiceDeletePrivatePluginVersionProcedure is the fully-qualified name of the
// PluginRegistryService's DeletePrivatePluginVersion RPC.
PluginRegistryServiceDeletePrivatePluginVersionProcedure = "/orchestrator.v1.PluginRegistryService/DeletePrivatePluginVersion"
// PluginRegistryServiceListPrivatePluginInstallSitesProcedure is the fully-qualified name of the
// PluginRegistryService's ListPrivatePluginInstallSites RPC.
PluginRegistryServiceListPrivatePluginInstallSitesProcedure = "/orchestrator.v1.PluginRegistryService/ListPrivatePluginInstallSites"
// PluginPublishServicePublishVersionProcedure is the fully-qualified name of the // PluginPublishServicePublishVersionProcedure is the fully-qualified name of the
// PluginPublishService's PublishVersion RPC. // PluginPublishService's PublishVersion RPC.
PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion" PluginPublishServicePublishVersionProcedure = "/orchestrator.v1.PluginPublishService/PublishVersion"
@ -87,6 +99,9 @@ const (
// PluginAuthServiceWhoamiProcedure is the fully-qualified name of the PluginAuthService's Whoami // PluginAuthServiceWhoamiProcedure is the fully-qualified name of the PluginAuthService's Whoami
// RPC. // RPC.
PluginAuthServiceWhoamiProcedure = "/orchestrator.v1.PluginAuthService/Whoami" PluginAuthServiceWhoamiProcedure = "/orchestrator.v1.PluginAuthService/Whoami"
// PluginAuthServiceListMyAccountsProcedure is the fully-qualified name of the PluginAuthService's
// ListMyAccounts RPC.
PluginAuthServiceListMyAccountsProcedure = "/orchestrator.v1.PluginAuthService/ListMyAccounts"
) )
// PluginScopeServiceClient is a client for the orchestrator.v1.PluginScopeService service. // PluginScopeServiceClient is a client for the orchestrator.v1.PluginScopeService service.
@ -219,6 +234,12 @@ type PluginRegistryServiceClient interface {
GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error) ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error)
ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error) ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error)
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
ListPrivatePlugins(context.Context, *connect.Request[v1.ListPrivatePluginsRequest]) (*connect.Response[v1.ListPrivatePluginsResponse], error)
DeletePrivatePlugin(context.Context, *connect.Request[v1.DeletePrivatePluginRequest]) (*connect.Response[v1.DeletePrivatePluginResponse], error)
DeletePrivatePluginVersion(context.Context, *connect.Request[v1.DeletePrivatePluginVersionRequest]) (*connect.Response[v1.DeletePrivatePluginVersionResponse], error)
ListPrivatePluginInstallSites(context.Context, *connect.Request[v1.ListPrivatePluginInstallSitesRequest]) (*connect.Response[v1.ListPrivatePluginInstallSitesResponse], error)
} }
// NewPluginRegistryServiceClient constructs a client for the orchestrator.v1.PluginRegistryService // NewPluginRegistryServiceClient constructs a client for the orchestrator.v1.PluginRegistryService
@ -268,17 +289,45 @@ func NewPluginRegistryServiceClient(httpClient connect.HTTPClient, baseURL strin
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")), connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithClientOptions(opts...), connect.WithClientOptions(opts...),
), ),
listPrivatePlugins: connect.NewClient[v1.ListPrivatePluginsRequest, v1.ListPrivatePluginsResponse](
httpClient,
baseURL+PluginRegistryServiceListPrivatePluginsProcedure,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListPrivatePlugins")),
connect.WithClientOptions(opts...),
),
deletePrivatePlugin: connect.NewClient[v1.DeletePrivatePluginRequest, v1.DeletePrivatePluginResponse](
httpClient,
baseURL+PluginRegistryServiceDeletePrivatePluginProcedure,
connect.WithSchema(pluginRegistryServiceMethods.ByName("DeletePrivatePlugin")),
connect.WithClientOptions(opts...),
),
deletePrivatePluginVersion: connect.NewClient[v1.DeletePrivatePluginVersionRequest, v1.DeletePrivatePluginVersionResponse](
httpClient,
baseURL+PluginRegistryServiceDeletePrivatePluginVersionProcedure,
connect.WithSchema(pluginRegistryServiceMethods.ByName("DeletePrivatePluginVersion")),
connect.WithClientOptions(opts...),
),
listPrivatePluginInstallSites: connect.NewClient[v1.ListPrivatePluginInstallSitesRequest, v1.ListPrivatePluginInstallSitesResponse](
httpClient,
baseURL+PluginRegistryServiceListPrivatePluginInstallSitesProcedure,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListPrivatePluginInstallSites")),
connect.WithClientOptions(opts...),
),
} }
} }
// pluginRegistryServiceClient implements PluginRegistryServiceClient. // pluginRegistryServiceClient implements PluginRegistryServiceClient.
type pluginRegistryServiceClient struct { type pluginRegistryServiceClient struct {
createPlugin *connect.Client[v1.CreatePluginRequest, v1.CreatePluginResponse] createPlugin *connect.Client[v1.CreatePluginRequest, v1.CreatePluginResponse]
getPlugin *connect.Client[v1.GetPluginRequest, v1.GetPluginResponse] getPlugin *connect.Client[v1.GetPluginRequest, v1.GetPluginResponse]
listPlugins *connect.Client[v1.ListPluginsRequest, v1.ListPluginsResponse] listPlugins *connect.Client[v1.ListPluginsRequest, v1.ListPluginsResponse]
getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse] getVersion *connect.Client[v1.GetVersionRequest, v1.GetVersionResponse]
resolveInstall *connect.Client[v1.ResolveInstallRequest, v1.ResolveInstallResponse] resolveInstall *connect.Client[v1.ResolveInstallRequest, v1.ResolveInstallResponse]
listCategories *connect.Client[v1.ListCategoriesRequest, v1.ListCategoriesResponse] listCategories *connect.Client[v1.ListCategoriesRequest, v1.ListCategoriesResponse]
listPrivatePlugins *connect.Client[v1.ListPrivatePluginsRequest, v1.ListPrivatePluginsResponse]
deletePrivatePlugin *connect.Client[v1.DeletePrivatePluginRequest, v1.DeletePrivatePluginResponse]
deletePrivatePluginVersion *connect.Client[v1.DeletePrivatePluginVersionRequest, v1.DeletePrivatePluginVersionResponse]
listPrivatePluginInstallSites *connect.Client[v1.ListPrivatePluginInstallSitesRequest, v1.ListPrivatePluginInstallSitesResponse]
} }
// CreatePlugin calls orchestrator.v1.PluginRegistryService.CreatePlugin. // CreatePlugin calls orchestrator.v1.PluginRegistryService.CreatePlugin.
@ -311,6 +360,28 @@ func (c *pluginRegistryServiceClient) ListCategories(ctx context.Context, req *c
return c.listCategories.CallUnary(ctx, req) return c.listCategories.CallUnary(ctx, req)
} }
// ListPrivatePlugins calls orchestrator.v1.PluginRegistryService.ListPrivatePlugins.
func (c *pluginRegistryServiceClient) ListPrivatePlugins(ctx context.Context, req *connect.Request[v1.ListPrivatePluginsRequest]) (*connect.Response[v1.ListPrivatePluginsResponse], error) {
return c.listPrivatePlugins.CallUnary(ctx, req)
}
// DeletePrivatePlugin calls orchestrator.v1.PluginRegistryService.DeletePrivatePlugin.
func (c *pluginRegistryServiceClient) DeletePrivatePlugin(ctx context.Context, req *connect.Request[v1.DeletePrivatePluginRequest]) (*connect.Response[v1.DeletePrivatePluginResponse], error) {
return c.deletePrivatePlugin.CallUnary(ctx, req)
}
// DeletePrivatePluginVersion calls
// orchestrator.v1.PluginRegistryService.DeletePrivatePluginVersion.
func (c *pluginRegistryServiceClient) DeletePrivatePluginVersion(ctx context.Context, req *connect.Request[v1.DeletePrivatePluginVersionRequest]) (*connect.Response[v1.DeletePrivatePluginVersionResponse], error) {
return c.deletePrivatePluginVersion.CallUnary(ctx, req)
}
// ListPrivatePluginInstallSites calls
// orchestrator.v1.PluginRegistryService.ListPrivatePluginInstallSites.
func (c *pluginRegistryServiceClient) ListPrivatePluginInstallSites(ctx context.Context, req *connect.Request[v1.ListPrivatePluginInstallSitesRequest]) (*connect.Response[v1.ListPrivatePluginInstallSitesResponse], error) {
return c.listPrivatePluginInstallSites.CallUnary(ctx, req)
}
// PluginRegistryServiceHandler is an implementation of the orchestrator.v1.PluginRegistryService // PluginRegistryServiceHandler is an implementation of the orchestrator.v1.PluginRegistryService
// service. // service.
type PluginRegistryServiceHandler interface { type PluginRegistryServiceHandler interface {
@ -320,6 +391,12 @@ type PluginRegistryServiceHandler interface {
GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error) GetVersion(context.Context, *connect.Request[v1.GetVersionRequest]) (*connect.Response[v1.GetVersionResponse], error)
ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error) ResolveInstall(context.Context, *connect.Request[v1.ResolveInstallRequest]) (*connect.Response[v1.ResolveInstallResponse], error)
ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error) ListCategories(context.Context, *connect.Request[v1.ListCategoriesRequest]) (*connect.Response[v1.ListCategoriesResponse], error)
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
ListPrivatePlugins(context.Context, *connect.Request[v1.ListPrivatePluginsRequest]) (*connect.Response[v1.ListPrivatePluginsResponse], error)
DeletePrivatePlugin(context.Context, *connect.Request[v1.DeletePrivatePluginRequest]) (*connect.Response[v1.DeletePrivatePluginResponse], error)
DeletePrivatePluginVersion(context.Context, *connect.Request[v1.DeletePrivatePluginVersionRequest]) (*connect.Response[v1.DeletePrivatePluginVersionResponse], error)
ListPrivatePluginInstallSites(context.Context, *connect.Request[v1.ListPrivatePluginInstallSitesRequest]) (*connect.Response[v1.ListPrivatePluginInstallSitesResponse], error)
} }
// NewPluginRegistryServiceHandler builds an HTTP handler from the service implementation. It // NewPluginRegistryServiceHandler builds an HTTP handler from the service implementation. It
@ -365,6 +442,30 @@ func NewPluginRegistryServiceHandler(svc PluginRegistryServiceHandler, opts ...c
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")), connect.WithSchema(pluginRegistryServiceMethods.ByName("ListCategories")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
pluginRegistryServiceListPrivatePluginsHandler := connect.NewUnaryHandler(
PluginRegistryServiceListPrivatePluginsProcedure,
svc.ListPrivatePlugins,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListPrivatePlugins")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceDeletePrivatePluginHandler := connect.NewUnaryHandler(
PluginRegistryServiceDeletePrivatePluginProcedure,
svc.DeletePrivatePlugin,
connect.WithSchema(pluginRegistryServiceMethods.ByName("DeletePrivatePlugin")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceDeletePrivatePluginVersionHandler := connect.NewUnaryHandler(
PluginRegistryServiceDeletePrivatePluginVersionProcedure,
svc.DeletePrivatePluginVersion,
connect.WithSchema(pluginRegistryServiceMethods.ByName("DeletePrivatePluginVersion")),
connect.WithHandlerOptions(opts...),
)
pluginRegistryServiceListPrivatePluginInstallSitesHandler := connect.NewUnaryHandler(
PluginRegistryServiceListPrivatePluginInstallSitesProcedure,
svc.ListPrivatePluginInstallSites,
connect.WithSchema(pluginRegistryServiceMethods.ByName("ListPrivatePluginInstallSites")),
connect.WithHandlerOptions(opts...),
)
return "/orchestrator.v1.PluginRegistryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return "/orchestrator.v1.PluginRegistryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case PluginRegistryServiceCreatePluginProcedure: case PluginRegistryServiceCreatePluginProcedure:
@ -379,6 +480,14 @@ func NewPluginRegistryServiceHandler(svc PluginRegistryServiceHandler, opts ...c
pluginRegistryServiceResolveInstallHandler.ServeHTTP(w, r) pluginRegistryServiceResolveInstallHandler.ServeHTTP(w, r)
case PluginRegistryServiceListCategoriesProcedure: case PluginRegistryServiceListCategoriesProcedure:
pluginRegistryServiceListCategoriesHandler.ServeHTTP(w, r) pluginRegistryServiceListCategoriesHandler.ServeHTTP(w, r)
case PluginRegistryServiceListPrivatePluginsProcedure:
pluginRegistryServiceListPrivatePluginsHandler.ServeHTTP(w, r)
case PluginRegistryServiceDeletePrivatePluginProcedure:
pluginRegistryServiceDeletePrivatePluginHandler.ServeHTTP(w, r)
case PluginRegistryServiceDeletePrivatePluginVersionProcedure:
pluginRegistryServiceDeletePrivatePluginVersionHandler.ServeHTTP(w, r)
case PluginRegistryServiceListPrivatePluginInstallSitesProcedure:
pluginRegistryServiceListPrivatePluginInstallSitesHandler.ServeHTTP(w, r)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@ -412,6 +521,22 @@ func (UnimplementedPluginRegistryServiceHandler) ListCategories(context.Context,
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListCategories is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListCategories is not implemented"))
} }
func (UnimplementedPluginRegistryServiceHandler) ListPrivatePlugins(context.Context, *connect.Request[v1.ListPrivatePluginsRequest]) (*connect.Response[v1.ListPrivatePluginsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListPrivatePlugins is not implemented"))
}
func (UnimplementedPluginRegistryServiceHandler) DeletePrivatePlugin(context.Context, *connect.Request[v1.DeletePrivatePluginRequest]) (*connect.Response[v1.DeletePrivatePluginResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.DeletePrivatePlugin is not implemented"))
}
func (UnimplementedPluginRegistryServiceHandler) DeletePrivatePluginVersion(context.Context, *connect.Request[v1.DeletePrivatePluginVersionRequest]) (*connect.Response[v1.DeletePrivatePluginVersionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.DeletePrivatePluginVersion is not implemented"))
}
func (UnimplementedPluginRegistryServiceHandler) ListPrivatePluginInstallSites(context.Context, *connect.Request[v1.ListPrivatePluginInstallSitesRequest]) (*connect.Response[v1.ListPrivatePluginInstallSitesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginRegistryService.ListPrivatePluginInstallSites is not implemented"))
}
// PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service. // PluginPublishServiceClient is a client for the orchestrator.v1.PluginPublishService service.
type PluginPublishServiceClient interface { type PluginPublishServiceClient interface {
PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error) PublishVersion(context.Context, *connect.Request[v1.PublishVersionRequest]) (*connect.Response[v1.PublishVersionResponse], error)
@ -491,6 +616,10 @@ type PluginAuthServiceClient interface {
DenyDevice(context.Context, *connect.Request[v1.DenyDeviceRequest]) (*connect.Response[v1.DenyDeviceResponse], error) DenyDevice(context.Context, *connect.Request[v1.DenyDeviceRequest]) (*connect.Response[v1.DenyDeviceResponse], error)
GetDeviceStatus(context.Context, *connect.Request[v1.GetDeviceStatusRequest]) (*connect.Response[v1.GetDeviceStatusResponse], error) GetDeviceStatus(context.Context, *connect.Request[v1.GetDeviceStatusRequest]) (*connect.Response[v1.GetDeviceStatusResponse], error)
Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error) Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error)
// ListMyAccounts returns the accounts the authenticated user belongs to.
// Used by `ninja login` (forced selection when multiple) and
// `ninja account list`.
ListMyAccounts(context.Context, *connect.Request[v1.ListMyAccountsRequest]) (*connect.Response[v1.ListMyAccountsResponse], error)
} }
// NewPluginAuthServiceClient constructs a client for the orchestrator.v1.PluginAuthService service. // NewPluginAuthServiceClient constructs a client for the orchestrator.v1.PluginAuthService service.
@ -540,6 +669,12 @@ func NewPluginAuthServiceClient(httpClient connect.HTTPClient, baseURL string, o
connect.WithSchema(pluginAuthServiceMethods.ByName("Whoami")), connect.WithSchema(pluginAuthServiceMethods.ByName("Whoami")),
connect.WithClientOptions(opts...), connect.WithClientOptions(opts...),
), ),
listMyAccounts: connect.NewClient[v1.ListMyAccountsRequest, v1.ListMyAccountsResponse](
httpClient,
baseURL+PluginAuthServiceListMyAccountsProcedure,
connect.WithSchema(pluginAuthServiceMethods.ByName("ListMyAccounts")),
connect.WithClientOptions(opts...),
),
} }
} }
@ -551,6 +686,7 @@ type pluginAuthServiceClient struct {
denyDevice *connect.Client[v1.DenyDeviceRequest, v1.DenyDeviceResponse] denyDevice *connect.Client[v1.DenyDeviceRequest, v1.DenyDeviceResponse]
getDeviceStatus *connect.Client[v1.GetDeviceStatusRequest, v1.GetDeviceStatusResponse] getDeviceStatus *connect.Client[v1.GetDeviceStatusRequest, v1.GetDeviceStatusResponse]
whoami *connect.Client[v1.WhoamiRequest, v1.WhoamiResponse] whoami *connect.Client[v1.WhoamiRequest, v1.WhoamiResponse]
listMyAccounts *connect.Client[v1.ListMyAccountsRequest, v1.ListMyAccountsResponse]
} }
// StartDevice calls orchestrator.v1.PluginAuthService.StartDevice. // StartDevice calls orchestrator.v1.PluginAuthService.StartDevice.
@ -583,6 +719,11 @@ func (c *pluginAuthServiceClient) Whoami(ctx context.Context, req *connect.Reque
return c.whoami.CallUnary(ctx, req) return c.whoami.CallUnary(ctx, req)
} }
// ListMyAccounts calls orchestrator.v1.PluginAuthService.ListMyAccounts.
func (c *pluginAuthServiceClient) ListMyAccounts(ctx context.Context, req *connect.Request[v1.ListMyAccountsRequest]) (*connect.Response[v1.ListMyAccountsResponse], error) {
return c.listMyAccounts.CallUnary(ctx, req)
}
// PluginAuthServiceHandler is an implementation of the orchestrator.v1.PluginAuthService service. // PluginAuthServiceHandler is an implementation of the orchestrator.v1.PluginAuthService service.
type PluginAuthServiceHandler interface { type PluginAuthServiceHandler interface {
StartDevice(context.Context, *connect.Request[v1.StartDeviceRequest]) (*connect.Response[v1.StartDeviceResponse], error) StartDevice(context.Context, *connect.Request[v1.StartDeviceRequest]) (*connect.Response[v1.StartDeviceResponse], error)
@ -591,6 +732,10 @@ type PluginAuthServiceHandler interface {
DenyDevice(context.Context, *connect.Request[v1.DenyDeviceRequest]) (*connect.Response[v1.DenyDeviceResponse], error) DenyDevice(context.Context, *connect.Request[v1.DenyDeviceRequest]) (*connect.Response[v1.DenyDeviceResponse], error)
GetDeviceStatus(context.Context, *connect.Request[v1.GetDeviceStatusRequest]) (*connect.Response[v1.GetDeviceStatusResponse], error) GetDeviceStatus(context.Context, *connect.Request[v1.GetDeviceStatusRequest]) (*connect.Response[v1.GetDeviceStatusResponse], error)
Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error) Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error)
// ListMyAccounts returns the accounts the authenticated user belongs to.
// Used by `ninja login` (forced selection when multiple) and
// `ninja account list`.
ListMyAccounts(context.Context, *connect.Request[v1.ListMyAccountsRequest]) (*connect.Response[v1.ListMyAccountsResponse], error)
} }
// NewPluginAuthServiceHandler builds an HTTP handler from the service implementation. It returns // NewPluginAuthServiceHandler builds an HTTP handler from the service implementation. It returns
@ -636,6 +781,12 @@ func NewPluginAuthServiceHandler(svc PluginAuthServiceHandler, opts ...connect.H
connect.WithSchema(pluginAuthServiceMethods.ByName("Whoami")), connect.WithSchema(pluginAuthServiceMethods.ByName("Whoami")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
pluginAuthServiceListMyAccountsHandler := connect.NewUnaryHandler(
PluginAuthServiceListMyAccountsProcedure,
svc.ListMyAccounts,
connect.WithSchema(pluginAuthServiceMethods.ByName("ListMyAccounts")),
connect.WithHandlerOptions(opts...),
)
return "/orchestrator.v1.PluginAuthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return "/orchestrator.v1.PluginAuthService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
case PluginAuthServiceStartDeviceProcedure: case PluginAuthServiceStartDeviceProcedure:
@ -650,6 +801,8 @@ func NewPluginAuthServiceHandler(svc PluginAuthServiceHandler, opts ...connect.H
pluginAuthServiceGetDeviceStatusHandler.ServeHTTP(w, r) pluginAuthServiceGetDeviceStatusHandler.ServeHTTP(w, r)
case PluginAuthServiceWhoamiProcedure: case PluginAuthServiceWhoamiProcedure:
pluginAuthServiceWhoamiHandler.ServeHTTP(w, r) pluginAuthServiceWhoamiHandler.ServeHTTP(w, r)
case PluginAuthServiceListMyAccountsProcedure:
pluginAuthServiceListMyAccountsHandler.ServeHTTP(w, r)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
@ -682,3 +835,7 @@ func (UnimplementedPluginAuthServiceHandler) GetDeviceStatus(context.Context, *c
func (UnimplementedPluginAuthServiceHandler) Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error) { func (UnimplementedPluginAuthServiceHandler) Whoami(context.Context, *connect.Request[v1.WhoamiRequest]) (*connect.Response[v1.WhoamiResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginAuthService.Whoami is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginAuthService.Whoami is not implemented"))
} }
func (UnimplementedPluginAuthServiceHandler) ListMyAccounts(context.Context, *connect.Request[v1.ListMyAccountsRequest]) (*connect.Response[v1.ListMyAccountsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("orchestrator.v1.PluginAuthService.ListMyAccounts is not implemented"))
}

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,31 @@ import (
"strings" "strings"
tomlpkg "github.com/BurntSushi/toml" tomlpkg "github.com/BurntSushi/toml"
v1 "git.dev.alexdunmow.com/block/core/internal/api/orchestrator/v1"
) )
// VisibilityLabel returns the lowercase, user-facing form of a PluginVisibility
// value (e.g. "private", "public", "under_review"). The default for the
// UNSPECIFIED zero value is "public", matching pre-enum behaviour for plugins
// that don't carry an explicit visibility.
func VisibilityLabel(v v1.PluginVisibility) string {
switch v {
case v1.PluginVisibility_PLUGIN_VISIBILITY_PRIVATE:
return "private"
case v1.PluginVisibility_PLUGIN_VISIBILITY_UNDER_REVIEW:
return "under_review"
case v1.PluginVisibility_PLUGIN_VISIBILITY_PUBLIC,
v1.PluginVisibility_PLUGIN_VISIBILITY_UNSPECIFIED:
return "public"
case v1.PluginVisibility_PLUGIN_VISIBILITY_REJECTED:
return "rejected"
case v1.PluginVisibility_PLUGIN_VISIBILITY_TAKEN_DOWN:
return "taken_down"
}
return "unknown"
}
type ModFile struct { type ModFile struct {
Plugin ModPlugin `toml:"plugin"` Plugin ModPlugin `toml:"plugin"`
Compatibility *ModCompat `toml:"compatibility"` Compatibility *ModCompat `toml:"compatibility"`
@ -30,6 +53,11 @@ type ModPlugin struct {
Version string `toml:"version"` Version string `toml:"version"`
Kind string `toml:"kind,omitempty"` Kind string `toml:"kind,omitempty"`
Categories []string `toml:"categories,omitempty"` Categories []string `toml:"categories,omitempty"`
// Private marks the plugin as account-scoped. When true, Coords() returns
// the canonical "@private/<name>@<version>" form regardless of the Scope
// field, and the publish flow attributes the plugin to the publisher's
// active account rather than to a public scope.
Private bool `toml:"private,omitempty"`
} }
type ModCompat struct { type ModCompat struct {
@ -60,9 +88,17 @@ func (m *ModFile) Coords() string {
if m == nil { if m == nil {
return "" return ""
} }
if m.Plugin.Private {
return "@" + PrivateScopeSlug + "/" + m.Plugin.Name + "@" + m.Plugin.Version
}
scope := strings.TrimPrefix(m.Plugin.Scope, "@") scope := strings.TrimPrefix(m.Plugin.Scope, "@")
if scope == "" { if scope == "" {
return m.Plugin.Name + "@" + m.Plugin.Version return m.Plugin.Name + "@" + m.Plugin.Version
} }
return "@" + scope + "/" + m.Plugin.Name + "@" + m.Plugin.Version return "@" + scope + "/" + m.Plugin.Name + "@" + m.Plugin.Version
} }
// PrivateScopeSlug is the registry namespace under which all private plugins
// live. Coords for private plugins resolve to "@private/<name>@<version>";
// uniqueness is enforced by (owner_account_id, name), not by the slug.
const PrivateScopeSlug = "private"

View File

@ -114,6 +114,61 @@ func TestCoords_AcceptsScopeWithOrWithoutAt(t *testing.T) {
} }
} }
func TestParseModFull_PrivateField(t *testing.T) {
src := []byte(`
[plugin]
name = "internal-tool"
version = "0.1.0"
private = true
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if !m.Plugin.Private {
t.Errorf("Private = false, want true")
}
}
func TestParseModFull_PrivateDefaultsFalse(t *testing.T) {
src := []byte(`
[plugin]
name = "public-thing"
scope = "themes"
version = "0.1.0"
`)
m, err := ParseModFull(src)
if err != nil {
t.Fatalf("ParseModFull err: %v", err)
}
if m.Plugin.Private {
t.Errorf("Private = true, want false (default)")
}
}
func TestCoords_PrivateOverridesScope(t *testing.T) {
m := &ModFile{Plugin: ModPlugin{
Name: "myplugin",
Scope: "@themes",
Version: "0.1.0",
Private: true,
}}
if got := m.Coords(); got != "@private/myplugin@0.1.0" {
t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got)
}
}
func TestCoords_PrivateNoScope(t *testing.T) {
m := &ModFile{Plugin: ModPlugin{
Name: "myplugin",
Version: "0.1.0",
Private: true,
}}
if got := m.Coords(); got != "@private/myplugin@0.1.0" {
t.Errorf("Coords() = %q, want @private/myplugin@0.1.0", got)
}
}
func TestParseModFull_RequiresAndCompat(t *testing.T) { func TestParseModFull_RequiresAndCompat(t *testing.T) {
src := []byte(` src := []byte(`
[plugin] [plugin]

View File

@ -21,6 +21,13 @@ service PluginRegistryService {
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); rpc GetVersion(GetVersionRequest) returns (GetVersionResponse);
rpc ResolveInstall(ResolveInstallRequest) returns (ResolveInstallResponse); rpc ResolveInstall(ResolveInstallRequest) returns (ResolveInstallResponse);
rpc ListCategories(ListCategoriesRequest) returns (ListCategoriesResponse); rpc ListCategories(ListCategoriesRequest) returns (ListCategoriesResponse);
// Private-plugin RPCs. All require the caller to be a member of the target
// account; the server resolves account membership from the bearer token.
rpc ListPrivatePlugins(ListPrivatePluginsRequest) returns (ListPrivatePluginsResponse);
rpc DeletePrivatePlugin(DeletePrivatePluginRequest) returns (DeletePrivatePluginResponse);
rpc DeletePrivatePluginVersion(DeletePrivatePluginVersionRequest) returns (DeletePrivatePluginVersionResponse);
rpc ListPrivatePluginInstallSites(ListPrivatePluginInstallSitesRequest) returns (ListPrivatePluginInstallSitesResponse);
} }
// PluginPublishService is called by the ninja CLI to publish a version. // PluginPublishService is called by the ninja CLI to publish a version.
@ -36,6 +43,10 @@ service PluginAuthService {
rpc DenyDevice(DenyDeviceRequest) returns (DenyDeviceResponse); rpc DenyDevice(DenyDeviceRequest) returns (DenyDeviceResponse);
rpc GetDeviceStatus(GetDeviceStatusRequest) returns (GetDeviceStatusResponse); rpc GetDeviceStatus(GetDeviceStatusRequest) returns (GetDeviceStatusResponse);
rpc Whoami(WhoamiRequest) returns (WhoamiResponse); rpc Whoami(WhoamiRequest) returns (WhoamiResponse);
// ListMyAccounts returns the accounts the authenticated user belongs to.
// Used by `ninja login` (forced selection when multiple) and
// `ninja account list`.
rpc ListMyAccounts(ListMyAccountsRequest) returns (ListMyAccountsResponse);
} }
// --- Shared messages --- // --- Shared messages ---
@ -47,11 +58,24 @@ message Scope {
google.protobuf.Timestamp created_at = 4; google.protobuf.Timestamp created_at = 4;
} }
// PluginVisibility is the lifecycle/access state of a plugin in the registry.
// PRIVATE plugins are scoped to a single account; PUBLIC plugins are visible
// to all. UNDER_REVIEW / REJECTED / TAKEN_DOWN are public-registry moderation
// states and do not apply to private plugins.
enum PluginVisibility {
PLUGIN_VISIBILITY_UNSPECIFIED = 0;
PLUGIN_VISIBILITY_PRIVATE = 1;
PLUGIN_VISIBILITY_UNDER_REVIEW = 2;
PLUGIN_VISIBILITY_PUBLIC = 3;
PLUGIN_VISIBILITY_REJECTED = 4;
PLUGIN_VISIBILITY_TAKEN_DOWN = 5;
}
message Plugin { message Plugin {
string id = 1; string id = 1;
string scope_slug = 2; string scope_slug = 2;
string name = 3; string name = 3;
string visibility = 4; PluginVisibility visibility = 4;
bool premium = 5; bool premium = 5;
string description = 6; string description = 6;
string homepage_url = 7; string homepage_url = 7;
@ -59,6 +83,19 @@ message Plugin {
google.protobuf.Timestamp updated_at = 9; google.protobuf.Timestamp updated_at = 9;
string kind = 10; string kind = 10;
string display_name = 11; string display_name = 11;
// owner_account_id is set for private plugins and identifies the account
// that owns the plugin. Empty for public plugins.
string owner_account_id = 12;
}
// Account is a multi-user organisational unit in the orchestrator. The
// authenticated user may belong to one or more accounts via account_users.
message Account {
string id = 1;
string slug = 2;
string display_name = 3;
// role of the authenticated caller in this account: owner | manager | member.
string role = 4;
} }
message Category { message Category {
@ -103,6 +140,14 @@ message CreatePluginRequest {
string kind = 4; string kind = 4;
repeated string categories = 5; repeated string categories = 5;
string display_name = 6; string display_name = 6;
// visibility is the requested visibility of the plugin. UNSPECIFIED defers
// to the registry's default (public for explicit scopes, private for the
// "@private" sentinel).
PluginVisibility visibility = 7;
// active_account_id is required when visibility = PRIVATE; the server
// verifies the caller is a member of this account before creating the
// private plugin under it.
string active_account_id = 8;
} }
message CreatePluginResponse { message CreatePluginResponse {
Plugin plugin = 1; Plugin plugin = 1;
@ -127,6 +172,58 @@ message ListPluginsResponse { repeated Plugin plugins = 1; }
message ListCategoriesRequest {} message ListCategoriesRequest {}
message ListCategoriesResponse { repeated Category categories = 1; } message ListCategoriesResponse { repeated Category categories = 1; }
// --- Private plugin RPC messages ---
message ListPrivatePluginsRequest {
// account_id selects which account's private plugins to list. The caller
// must be a member of this account.
string account_id = 1;
}
message ListPrivatePluginsResponse {
repeated PrivatePluginSummary plugins = 1;
}
// PrivatePluginSummary is the row shape used by the publisher dashboard and
// by the CMS "Private" installer tab. It bundles a Plugin with the latest
// version per channel and an installation count.
message PrivatePluginSummary {
Plugin plugin = 1;
// channel_versions maps channel name -> latest version string published on
// that channel (e.g. "latest" -> "0.3.0", "beta" -> "0.4.0-beta.1").
map<string,string> channel_versions = 2;
// installed_site_count is the number of CMS sites in the owning account
// that currently have any version of this plugin installed. Used to gate
// delete-plugin in the dashboard.
int32 installed_site_count = 3;
}
message DeletePrivatePluginRequest {
string account_id = 1;
string plugin_name = 2;
}
message DeletePrivatePluginResponse {}
message DeletePrivatePluginVersionRequest {
string account_id = 1;
string plugin_name = 2;
string version = 3;
}
message DeletePrivatePluginVersionResponse {}
message ListPrivatePluginInstallSitesRequest {
string account_id = 1;
string plugin_name = 2;
}
message ListPrivatePluginInstallSitesResponse {
repeated PrivatePluginInstallSite sites = 1;
}
message PrivatePluginInstallSite {
string instance_id = 1;
string site_display_name = 2;
string installed_version = 3;
string pinned_channel = 4;
}
message GetVersionRequest { message GetVersionRequest {
string scope_slug = 1; string scope_slug = 1;
string plugin_name = 2; string plugin_name = 2;
@ -211,3 +308,8 @@ message WhoamiResponse {
string email = 2; string email = 2;
string display_name = 3; string display_name = 3;
} }
message ListMyAccountsRequest {}
message ListMyAccountsResponse {
repeated Account accounts = 1;
}