RPC Docs.
Prim+RPC does not have a stable release and should not yet be used in production applications. Please report any security issues that you find according to the security policy.
Prim+RPC's goal is simple: write a function on the server and call it from the client. Once Prim+RPC is set up, it is incredibly easy to add new functions to the backend. However caution should be taken since input given to the server cannot be trusted. Tasks such as validation, sanitation, authentication, and more are outside of Prim+RPC's responsibilities and are left to the developer (and the libraries that you choose).
When you pass a function to the .module
option of Prim+RPC, it is intended to become RPC. By default however you will
be denied access to execute the function remotely on the client until you explicitly mark it as an RPC. This is done in
one of two ways: by setting the .rpc
property on the function to true
or by adding your function to the .allowList
option of the Prim+RPC server.
import { createPrimServer } from "@doseofted/prim-rpc"// This can be called from the client because we set the `.rpc` property to `true`function myPublicFunction() { return "I'm allowed to be called from the client"}myPublicFunction.rpc = true// This cannot be called from the client even though we passed it to the client// (note: type definitions are still shared because is is given to server below)function myPrivateFunction() { return "I'm only allowed to be called on the server"}// While we cannot add an `.rpc` property directly, we can add it to the allow list below// We could alternatively create a wrapper function with an `.rpc` property that calls `myFrozenFunction()`function myFrozenFunction() { return "I'm allowed to be called from the client"}Object.freeze(myFrozenFunction)createPrimServer({ module: { myPublicFunction, myPrivateFunction, myFrozenFunction }, allowList: { myFrozenFunction: true },})
Prim+RPC does not validate or sanitize arguments passed to the server. It is up to the developer to ensure that the arguments provided are of the expected type and shape. You may choose a validation library of your choice to accomplish this. Without a validation library, it should be expected that types given from the client could not match your type definitions. Consider the following:
In this example a user is subscribed without their consent because the string no
is a truthy value. This can be
avoided with validation of given arguments. You may choose any library that you'd like to validate arguments. We'll use
Zod in this example but you could also use libraries like TypeBox, ArkType, or many others. Below is a safer example:
import { subscribeToNewsLetter } from "./my-newsletter-service"import { z } from "zod"const formInputsSchema = z.object({ email: z.string().trim().email(), subscribe: z.boolean().default(false),})// this is only a simple demonstrationexport function submitForm(givenForm: z.infer<typeof formInputsSchema>) { const form = formInputsSchema.parse(givenForm) return form.subscribe ? subscribeToNewsLetter(form.email) : false}submitForm.rpc = truecreatePrimServer({ module: { submitForm } })
Now when we submit this form, we will be presented an error because the client did not provide the boolean value that is expected. If you're coming from tRPC, you may consider using Zod's syntax for defining a function (which bears some resemblance to defining a tRPC router):
import { subscribeToNewsLetter } from "./my-newsletter-service"import { z } from "zod"// this is only a simple demonstrationconst submitForm = z .function() .args( z.object({ email: z.string().trim().email(), subscribe: z.boolean().default(false), }) ) .returns(z.boolean()) .implements(form => { return form.subscribe ? subscribeToNewsLetter(form.email) : false })submitForm.rpc = truecreatePrimServer({ module: { submitForm } })
By default, Prim+RPC will use the environment's default JSON handler for serialization and unjs/destr for deserialization (which provides the benefit of protection from prototype pollution while behaving predictably).
You may override the JSON handler with your own as you'd like. This may provide support for additional types or may even be used to serialize to a format other than JSON. However be aware that a new handler could introduce new security issues and this should be considered, especially if RPC is intended to be shared over public channels.
Prim+RPC keeps a narrow scope: it handles RPC but it utilizes separate plugins for transport. This means that you can choose any transport that you'd like by using an available plugin or creating your own but it means that the security of the transport falls outside of Prim+RPC's scope.
Securing the transport may mean something different depending on the environment. For instance, if you're using Prim+RPC on a web server then you may consider using TLS, CORS headers, rate limiting, authentication, and other means. On a web client, you may consider the possibility of XSS.