Brings the uncommitted body of work under version control so it cannot be
lost. Gates are green: 152 tests pass across rpc/router/dev-server,
typecheck, lint, format and check:public-api all clean.
NOT YET REVIEWED. None of Tasks 5-11 has had an independent task review, and
Task 4's second fix round was never re-reviewed either.
Known gaps against the plan, recorded here rather than discovered later:
- packages/rpc/test/{transport,server,client}.test.ts are ABSENT. The plan
required a test file for each. server.ts holds the fail-closed identity and
permission checks and currently has no direct coverage at all.
- rpc-endpoint.test.ts has 3 tests where the plan specified 9. Missing:
unknown service, non-POST, malformed body, non-rpc passthrough, and the
isInternalCaller sweep. This is the task where a reachable
/__wrnexus/rpc/* makes every permission check in the workspace bypassable.
- http.test.ts has 3 of 7; integration.test.ts 2 of 3;
services-discovery.test.ts 1 of 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
import { RPC_ERROR_CODES, failure, success } from "./errors.ts";
|
|
import { importSubjectContext, type SubjectContext } from "./identity.ts";
|
|
import type {
|
|
AnyProcedures,
|
|
InferProcedureInput,
|
|
InferProcedureOutput,
|
|
ServiceContract,
|
|
ServiceResult,
|
|
} from "./types.ts";
|
|
|
|
export interface HandlerContext {
|
|
subject?: SubjectContext;
|
|
}
|
|
|
|
export type ServiceHandlers<Procedures extends AnyProcedures> = {
|
|
[K in keyof Procedures]: (
|
|
input: InferProcedureInput<Procedures[K]>,
|
|
ctx: HandlerContext,
|
|
) => Promise<InferProcedureOutput<Procedures[K]>> | InferProcedureOutput<Procedures[K]>;
|
|
};
|
|
|
|
export interface ImplementOptions {
|
|
selfApp: string;
|
|
checkPermission?: (permission: string, subject?: SubjectContext) => Promise<boolean> | boolean;
|
|
}
|
|
|
|
export interface ServiceImplementation<Procedures extends AnyProcedures = AnyProcedures> {
|
|
contract: ServiceContract<Procedures>;
|
|
invoke(procedure: string, payload: unknown, identity?: string): Promise<ServiceResult>;
|
|
}
|
|
|
|
export function implement<Procedures extends AnyProcedures>(
|
|
contract: ServiceContract<Procedures>,
|
|
handlers: ServiceHandlers<Procedures>,
|
|
options: ImplementOptions,
|
|
): ServiceImplementation<Procedures> {
|
|
return {
|
|
contract,
|
|
async invoke(procedureName, payload, identity) {
|
|
const definition = contract.procedures[procedureName as keyof Procedures];
|
|
const handler = handlers[procedureName as keyof Procedures];
|
|
if (!definition || !handler) return failure(RPC_ERROR_CODES.unknown, "Unknown procedure");
|
|
|
|
let subject: SubjectContext | undefined;
|
|
if (identity !== undefined) {
|
|
try {
|
|
subject = await importSubjectContext(identity, options.selfApp);
|
|
} catch {
|
|
return failure(RPC_ERROR_CODES.identity, "Invalid identity");
|
|
}
|
|
}
|
|
|
|
if (definition.permission) {
|
|
if (!options.checkPermission) return failure(RPC_ERROR_CODES.denied, "Forbidden");
|
|
try {
|
|
if (!(await options.checkPermission(definition.permission, subject))) {
|
|
return failure(RPC_ERROR_CODES.denied, "Forbidden");
|
|
}
|
|
} catch {
|
|
return failure(RPC_ERROR_CODES.denied, "Forbidden");
|
|
}
|
|
}
|
|
|
|
let input: unknown = payload;
|
|
if (definition.input) {
|
|
const parsed = definition.input.parse(payload as Record<string, unknown>);
|
|
if (!parsed.ok) return failure(RPC_ERROR_CODES.invalid, "Invalid input");
|
|
input = parsed.value;
|
|
}
|
|
|
|
try {
|
|
const value = await (handler as (value: unknown, ctx: HandlerContext) => unknown)(input, {
|
|
subject,
|
|
});
|
|
return success(value);
|
|
} catch {
|
|
return failure(RPC_ERROR_CODES.handler, "Internal error");
|
|
}
|
|
},
|
|
};
|
|
}
|