You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PluginServiceConfig exposes 16 knobs — cors, logger, openapi, docs, database, middleware, context,
rpc, health, raw routes, startup/shutdown hooks — and no auth field. createPluginService never
calls withAuthn or withAuthz. Consequently the five first-party plugin services (auth, workers,
sagas, streams, triggers) and every plugin produced by netscript plugin new publish /api/rpc/v1/<ns>/* and /api/v1/<ns>/* with no possible guard short of forking the service
entrypoint. The auth plugin is the sharpest case: the service that owns identity is itself
unauthenticated (see #1384).
packages/plugin/src/service/presentation/create-plugin-service.ts:63-104 — the full PluginServiceConfig interface; no auth, no authn, no authz, no principal.
grep -rn 'withAuthn\|withAuthz' packages plugins outside packages/service/ returns zero
matches — no plugin, and no scaffolded service, calls the guard.
Callers proven unguarded today: plugins/auth/services/src/main.ts:70-84, plugins/workers/services/src/main.ts, plugins/sagas/services/src/main.ts, plugins/streams/services/src/main.ts, plugins/triggers/services/src/main.ts, plus packages/cli/src/public/features/plugins/new/new-plugin-use-case.ts (the generator for
third-party plugins).
The guard that is missing already exists and is correct: packages/service/src/auth/auth-middleware.ts:19,22
(DEFAULT_PROTECTED_PREFIXES = ['/api'], DEFAULT_ANONYMOUS_PREFIXES = ['/health']),
fail-closed authz (packages/service/src/auth/scope-authorizer.ts:41).
No remote session-verifying authenticator exists: the only AuthenticatorPort implementations are static-credential, trusted-header, kv-oauth, workos, better-auth (auth.md §4.5), so a
service can only validate the auth plugin's sessions by embedding the backend, its KV/DB handles
and its provider secrets in-process.
Current surface
A plugin author has exactly two options: publish an open API, or abandon createPluginService and
hand-assemble a ServiceBuilder — which discards the "mandated builder order" the factory exists to
enforce (create-plugin-service.ts:109-112). createTrustedHeaderAuthenticator reads x-authenticated-user/-scopes/-roles (packages/service/src/auth/trusted-header-authenticator.ts:20-54)
but nothing in the repo emits those headers — the receiver exists, the sender does not
(auth.md §4.4, gap G14).
Target contract
PluginServiceConfig gains auth?: PluginServiceAuthConfig carrying { authn, authz? } with the
same shape defineService already accepts (packages/service/src/presets/define-service.ts:268-273),
applied by createPluginServicebeforewithRPC so route registration stays inside the guard —
the ordering packages/service/src/builder/service-builder-impl.ts:435-436 already guarantees
(installAuth() at :442 runs before installDeferredRoutes() at :462).
@netscript/plugin-auth exports createAuthServiceAuthenticator({ serviceName, ... }) — an AuthenticatorPort that verifies a request by calling the auth service's GET /session over the
typed client ([sdk-client S5] feat(sdk/auth): prove typed credential contributions end to end #1352 seam), so a guarded service needs no backend, no KV handle and no provider
secret. This is gap G8 and it is what makes the plugin composable at all.
The five first-party plugin services adopt the seam; each declares its own scope rules, and /health stays anonymous.
netscript plugin new generates the guarded form.
Acceptance
PluginServiceConfig.auth exists and createPluginService applies it before withRPC.
Omitting auth requires an explicit recorded public opt-out.
@netscript/plugin-auth exports a remote session-verifying AuthenticatorPort.
The remote authenticator needs no backend instance, KV handle, or provider secret.
All five first-party plugin services declare an auth configuration or a recorded opt-out.
netscript plugin new scaffolds a guarded plugin service.
Negative test: an unauthenticated POST /api/rpc/v1/<ns>/* on a guarded plugin service returns 401.
Negative test: an unauthenticated GET /api/v1/<ns>/* on a guarded plugin service returns 401.
Negative test: a valid principal with an insufficient scope returns 403 on the same routes.
Negative test: the guard cannot be bypassed by reordering config fields (order is factory-owned).
A guard test fails if a future createPluginService change registers RPC routes before authn.
gate: deno task e2e:cli run scaffold.runtime --cleanup green with guarded plugin services.
docs/site/explanation/plugin-system.md and docs/site/identity-access/auth.md must show a plugin
service declaring auth and a non-auth plugin verifying sessions through createAuthServiceAuthenticator without embedding a backend. Adoption is proven when a third-party
plugin fixture (not a first-party one) is guarded end to end in a test, and when docs/site/identity-access/how-to/add-authentication.md can state, with a code reference, how to
protect a plugin API — a sentence it cannot write today.
Provenance
Seed run plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced from research/repo-audit/auth.md gaps G2/G8/G14; all cited line numbers re-verified against worktree fac9e339042c on 2026-08-08.
Filed from planning seed PR #1347 · source Draft-ID TA-02 · live issue #1383.
Summary
PluginServiceConfigexposes 16 knobs — cors, logger, openapi, docs, database, middleware, context,rpc, health, raw routes, startup/shutdown hooks — and no auth field.
createPluginServicenevercalls
withAuthnorwithAuthz. Consequently the five first-party plugin services (auth, workers,sagas, streams, triggers) and every plugin produced by
netscript plugin newpublish/api/rpc/v1/<ns>/*and/api/v1/<ns>/*with no possible guard short of forking the serviceentrypoint. The auth plugin is the sharpest case: the service that owns identity is itself
unauthenticated (see #1384).
Evidence
research/repo-audit/auth.md§3.1, gaps G2 and G8; SYNTHESIS §2("Plugin-composition failure").
packages/plugin/src/service/presentation/create-plugin-service.ts:63-104— the fullPluginServiceConfiginterface; noauth, noauthn, noauthz, noprincipal.packages/plugin/src/service/presentation/create-plugin-service.ts:137-194— the fixed chain:createService → withCors → withLogger → withOpenAPI → withDocs → withDatabase → use(middleware) → withContext → withRPC → withHealth → withServiceInfo → route → onStartup → onShutdown. No authstage exists.
grep -rn 'withAuthn\|withAuthz' packages pluginsoutsidepackages/service/returns zeromatches — no plugin, and no scaffolded service, calls the guard.
plugins/auth/services/src/main.ts:70-84,plugins/workers/services/src/main.ts,plugins/sagas/services/src/main.ts,plugins/streams/services/src/main.ts,plugins/triggers/services/src/main.ts, pluspackages/cli/src/public/features/plugins/new/new-plugin-use-case.ts(the generator forthird-party plugins).
packages/service/src/auth/auth-middleware.ts:19,22(
DEFAULT_PROTECTED_PREFIXES = ['/api'],DEFAULT_ANONYMOUS_PREFIXES = ['/health']),fail-closed authz (
packages/service/src/auth/scope-authorizer.ts:41).AuthenticatorPortimplementations arestatic-credential,trusted-header,kv-oauth,workos,better-auth(auth.md§4.5), so aservice can only validate the auth plugin's sessions by embedding the backend, its KV/DB handles
and its provider secrets in-process.
Current surface
A plugin author has exactly two options: publish an open API, or abandon
createPluginServiceandhand-assemble a
ServiceBuilder— which discards the "mandated builder order" the factory exists toenforce (
create-plugin-service.ts:109-112).createTrustedHeaderAuthenticatorreadsx-authenticated-user/-scopes/-roles(packages/service/src/auth/trusted-header-authenticator.ts:20-54)but nothing in the repo emits those headers — the receiver exists, the sender does not
(
auth.md§4.4, gap G14).Target contract
PluginServiceConfiggainsauth?: PluginServiceAuthConfigcarrying{ authn, authz? }with thesame shape
defineServicealready accepts (packages/service/src/presets/define-service.ts:268-273),applied by
createPluginServicebeforewithRPCso route registration stays inside the guard —the ordering
packages/service/src/builder/service-builder-impl.ts:435-436already guarantees(
installAuth()at:442runs beforeinstallDeferredRoutes()at:462).authis an explicit decision, not a default. The factoryrequires either
author a recordedauth: 'public'opt-out, mirroring fix(scaffold): the generated service ships/apiunprotected and a framework test codifies public API routes as correct #1382 so the frameworkhas one rule for both generated and plugin services.
@netscript/plugin-authexportscreateAuthServiceAuthenticator({ serviceName, ... })— anAuthenticatorPortthat verifies a request by calling the auth service'sGET /sessionover thetyped client ([sdk-client S5] feat(sdk/auth): prove typed credential contributions end to end #1352 seam), so a guarded service needs no backend, no KV handle and no provider
secret. This is gap G8 and it is what makes the plugin composable at all.
/healthstays anonymous.netscript plugin newgenerates the guarded form.Acceptance
PluginServiceConfig.authexists andcreatePluginServiceapplies it beforewithRPC.authrequires an explicit recorded public opt-out.@netscript/plugin-authexports a remote session-verifyingAuthenticatorPort.netscript plugin newscaffolds a guarded plugin service.POST /api/rpc/v1/<ns>/*on a guarded plugin service returns 401.GET /api/v1/<ns>/*on a guarded plugin service returns 401.createPluginServicechange registers RPC routes before authn.deno task e2e:cli run scaffold.runtime --cleanupgreen with guarded plugin services.Boundaries
per-procedure route table at
/api/plugins/<mountId>/, its CSRF/origin checks and its threatmodel. This issue guards the plugin's own service surface; [frontend-contrib S12] Generated deny-by-default procedure gateway #934 guards the browser-facing
projection of it. Align the deny-by-default vocabulary with [frontend-contrib S12] Generated deny-by-default procedure gateway #934, do not implement it here.
authorizer added here stays scope/role-based and must remain adaptable to [enterprise-auth S13] Define organization-aware identity and authorization policy contracts #884's model rather than
competing with it.
POST /api/v1/auth/signoutrevokes any session id an unauthenticated caller supplies #1384 owns it (thisissue supplies the seam it consumes).
localhost:4437auth session URL — auth: session list --stream-url default pins localhost:4437 which no longer exists post-#1211 #1243 owns it.Docs/consumer proof
docs/site/explanation/plugin-system.mdanddocs/site/identity-access/auth.mdmust show a pluginservice declaring
authand a non-auth plugin verifying sessions throughcreateAuthServiceAuthenticatorwithout embedding a backend. Adoption is proven when a third-partyplugin fixture (not a first-party one) is guarded end to end in a test, and when
docs/site/identity-access/how-to/add-authentication.mdcan state, with a code reference, how toprotect a plugin API — a sentence it cannot write today.
Provenance
Seed run
plan-fable5-remediation-roadmap--seed, PR #1347, 2026-08-08. Sourced fromresearch/repo-audit/auth.mdgaps G2/G8/G14; all cited line numbers re-verified against worktreefac9e339042con 2026-08-08.Filed from planning seed PR #1347 · source Draft-ID TA-02 · live issue #1383.