Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,5 @@ backend accepts only while it allows legacy anonymous calls. Set all of these or
- `OAUTH_BACKEND_RESOURCE_URI`: the backend's resource identifier, the token's audience
- `OAUTH_WEBSITE_CLIENT_ID` and `OAUTH_WEBSITE_PRIVATE_JWK` (secret): the client and its private signing key as a JWK

`groups.search.*` stays anonymous until the backend accepts the website's token there.

## Contributing
If you are contributing, you may check [GUIDES.md](./GUIDES.md)
27 changes: 8 additions & 19 deletions src/lib/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@ import "server-only"

import { idpEndpoints, type ServiceTokenSourceOptions, serviceTokenSource } from "@polinetwork/auth-kit"
import { type AppRouter, TRPC_PATH } from "@polinetwork/backend"
import { createTRPCClient, httpBatchLink, retryLink, splitLink } from "@trpc/client"
import { createTRPCClient, httpBatchLink, retryLink } from "@trpc/client"
import SuperJSON from "superjson"
import { env } from "@/env"

/** Scope of the `website` client (RFC v3 §6.2). */
const PUBLIC_READ_SCOPE = "backend:public:read"

/**
* Procedures the backend does not yet open to `backend:public:read`: a service token is refused
* there, so they stay on the anonymous legacy path until the backend accepts the website's token.
*/
const ANONYMOUS_PROCEDURES = new Set(["groups.search.getAll", "groups.search.search"])

function parsePrivateKey(value: string): ServiceTokenSourceOptions["privateKey"] {
try {
return JSON.parse(value)
Expand Down Expand Up @@ -49,29 +43,24 @@ function createTokenSource() {

const tokens = createTokenSource()
const url = env.BACKEND_URL + TRPC_PATH
const anonymousLink = httpBatchLink({ url, transformer: SuperJSON })

export const trpc = createTRPCClient<AppRouter>({
links: tokens
? [
// A 401 is decided before the procedure runs, so retrying once with a new token is safe for
// mutations too. The backend may have rejected an expired or revoked token we still cached.
retryLink({
retry: ({ op, error, attempts }) => {
if (attempts > 1 || ANONYMOUS_PROCEDURES.has(op.path) || error.data?.httpStatus !== 401) return false
retry: ({ error, attempts }) => {
if (attempts > 1 || error.data?.httpStatus !== 401) return false
tokens.invalidate()
return true
},
}),
splitLink({
condition: (op) => ANONYMOUS_PROCEDURES.has(op.path),
true: anonymousLink,
false: httpBatchLink({
url,
transformer: SuperJSON,
headers: async () => ({ Authorization: `Bearer ${await tokens.getToken()}` }),
}),
httpBatchLink({
url,
transformer: SuperJSON,
headers: async () => ({ Authorization: `Bearer ${await tokens.getToken()}` }),
}),
]
: [anonymousLink],
: [httpBatchLink({ url, transformer: SuperJSON })],
})
Loading