@webstudio-is/trpc-interface 0.90.0 → 0.260.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +26 -25
  2. package/src/authorize/project.server.test.ts +443 -0
  3. package/src/authorize/project.server.ts +309 -121
  4. package/src/authorize/role.ts +18 -0
  5. package/src/context/context.server.ts +59 -24
  6. package/src/context/errors.server.ts +16 -0
  7. package/src/context/router.server.ts +19 -0
  8. package/src/index.server.ts +15 -3
  9. package/src/shared/client.ts +0 -2
  10. package/src/shared/deployment.ts +23 -6
  11. package/src/shared/domain.ts +3 -3
  12. package/src/shared/plan-client.server.ts +7 -0
  13. package/src/shared/plan-features.ts +7 -0
  14. package/src/shared/shared-router.ts +0 -2
  15. package/src/shared/trpc.ts +5 -1
  16. package/src/trpc-caller-link.test.ts +1 -1
  17. package/src/trpc-caller-link.ts +1 -2
  18. package/tsconfig.json +3 -0
  19. package/lib/authorize/authorization-token.server.js +0 -72
  20. package/lib/authorize/project.server.js +0 -103
  21. package/lib/cjs/authorize/authorization-token.server.js +0 -92
  22. package/lib/cjs/authorize/project.server.js +0 -123
  23. package/lib/cjs/context/context.server.js +0 -16
  24. package/lib/cjs/context/errors.server.js +0 -29
  25. package/lib/cjs/index.js +0 -18
  26. package/lib/cjs/index.server.js +0 -40
  27. package/lib/cjs/package.json +0 -1
  28. package/lib/cjs/shared/authorization-router.js +0 -184
  29. package/lib/cjs/shared/client.js +0 -63
  30. package/lib/cjs/shared/deployment.js +0 -51
  31. package/lib/cjs/shared/domain.js +0 -98
  32. package/lib/cjs/shared/shared-router.js +0 -32
  33. package/lib/cjs/shared/trpc.js +0 -31
  34. package/lib/cjs/trpc-caller-link.js +0 -46
  35. package/lib/context/context.server.js +0 -0
  36. package/lib/context/errors.server.js +0 -9
  37. package/lib/index.js +0 -1
  38. package/lib/index.server.js +0 -10
  39. package/lib/shared/authorization-router.js +0 -164
  40. package/lib/shared/client.js +0 -35
  41. package/lib/shared/deployment.js +0 -31
  42. package/lib/shared/domain.js +0 -78
  43. package/lib/shared/shared-router.js +0 -12
  44. package/lib/shared/trpc.js +0 -11
  45. package/lib/trpc-caller-link.js +0 -26
  46. package/lib/types/authorize/authorization-token.server.d.ts +0 -21
  47. package/lib/types/authorize/project.server.d.ts +0 -25
  48. package/lib/types/context/context.server.d.ts +0 -53
  49. package/lib/types/context/errors.server.d.ts +0 -1
  50. package/lib/types/index.d.ts +0 -1
  51. package/lib/types/index.server.d.ts +0 -7
  52. package/lib/types/shared/authorization-router.d.ts +0 -276
  53. package/lib/types/shared/client.d.ts +0 -8
  54. package/lib/types/shared/deployment.d.ts +0 -45
  55. package/lib/types/shared/domain.d.ts +0 -119
  56. package/lib/types/shared/shared-router.d.ts +0 -415
  57. package/lib/types/shared/trpc.d.ts +0 -48
  58. package/lib/types/trpc-caller-link.d.ts +0 -16
  59. package/lib/types/trpc-caller-link.test.d.ts +0 -49
  60. package/src/authorize/authorization-token.server.ts +0 -106
  61. package/src/shared/authorization-router.ts +0 -198
@@ -1,129 +1,321 @@
1
- import type { Project } from "@webstudio-is/prisma-client";
2
- import type { AuthPermit } from "../shared/authorization-router";
3
1
  import type { AppContext } from "../context/context.server";
2
+ import type { Role } from "./role";
3
+ import memoize from "memoize";
4
4
 
5
- /**
6
- * For 3rd party authorization systems like Ory we need to register the project owner.
7
- *
8
- * We do that before the project create (and out of the transaction),
9
- * so in case of an error we will have just stale records of non existed projects in authorization system.
10
- */
11
- export const registerProjectOwner = async (
12
- props: { projectId: string },
13
- context: AppContext
14
- ) => {
15
- const { authorization } = context;
16
- const { userId, authorizeTrpc } = authorization;
5
+ type Relation = Role;
17
6
 
18
- if (userId === undefined) {
19
- throw new Error("The user must be authenticated to create a project");
20
- }
7
+ export type AuthPermit = "view" | "edit" | "build" | "admin" | "own";
21
8
 
22
- await authorizeTrpc.create.mutate({
23
- namespace: "Project",
24
- id: props.projectId,
25
- relation: "owners",
26
- subjectSet: {
27
- namespace: "User",
28
- id: userId,
29
- },
30
- });
9
+ type TokenAuthPermit = Exclude<AuthPermit, "own">;
10
+
11
+ type CheckInput = {
12
+ namespace: "Project";
13
+ id: string;
14
+
15
+ permit: AuthPermit;
16
+
17
+ subjectSet: {
18
+ namespace: "User" | "Token";
19
+ id: string;
20
+ };
31
21
  };
32
22
 
33
- export const hasProjectPermit = async (
34
- props: {
35
- projectId: Project["id"];
36
- permit: AuthPermit;
37
- },
38
- context: AppContext
39
- ) => {
40
- const start = Date.now();
23
+ const permitToRelationRewrite: Record<TokenAuthPermit, Relation[]> = {
24
+ view: ["viewers", "editors", "builders", "administrators"],
25
+ edit: ["editors", "builders", "administrators"],
26
+ build: ["builders", "administrators"],
27
+ admin: ["administrators"],
28
+ };
41
29
 
42
- try {
43
- const { authorization } = context;
44
- const { authorizeTrpc } = authorization;
30
+ /**
31
+ * Pure function: checks whether a set of workspace relations grants a given
32
+ * permit. Used by the auth layer to evaluate workspace-based access.
33
+ */
34
+ const isRolePermitted = (relations: string[], permit: AuthPermit): boolean => {
35
+ // Workspace owner gets all permits
36
+ if (relations.includes("own")) {
37
+ return true;
38
+ }
39
+ // Only workspace owner gets "own" permit
40
+ if (permit === "own") {
41
+ return false;
42
+ }
43
+ const permitted = permitToRelationRewrite[permit] ?? [];
44
+ return relations.some((r) => permitted.includes(r as Relation));
45
+ };
45
46
 
46
- const checks = [];
47
- const namespace = "Project";
47
+ const check = async (
48
+ postgrestClient: AppContext["postgrest"]["client"],
49
+ input: CheckInput
50
+ ) => {
51
+ const { subjectSet } = input;
48
52
 
49
- // Allow load production build env i.e. "published" site
50
- if (props.permit === "view" && context.authorization.isServiceCall) {
51
- return true;
53
+ if (subjectSet.namespace === "User") {
54
+ // Check if the user is the direct owner of the project
55
+ const row = await postgrestClient
56
+ .from("Project")
57
+ .select("id")
58
+ .eq("id", input.id)
59
+ .eq("userId", subjectSet.id)
60
+ .maybeSingle();
61
+ if (row.error) {
62
+ throw row.error;
52
63
  }
53
64
 
54
- // Allow load webstudiois for clone
55
- // @todo Rethink permissions for this use-case
56
- // The plan is to make new permission for projects which are allowed to be publicly clonable by anyone
57
- // https://github.com/webstudio-is/webstudio-builder/issues/1038
58
- if (
59
- props.permit === "view" &&
60
- props.projectId === "62154aaef0cb0860ccf85d6e"
61
- ) {
62
- return true;
65
+ if (row.data !== null) {
66
+ return { allowed: true };
63
67
  }
64
68
 
65
- if (
66
- props.permit === "view" &&
67
- context.authorization.projectTemplates.includes(props.projectId)
68
- ) {
69
- return true;
69
+ // Workspace-based authorization
70
+ const wpaRows = await postgrestClient
71
+ .from("WorkspaceProjectAuthorization")
72
+ .select("relation")
73
+ .eq("userId", subjectSet.id)
74
+ .eq("projectId", input.id);
75
+
76
+ if (wpaRows.error) {
77
+ throw wpaRows.error;
70
78
  }
71
79
 
72
- // Check if the user is allowed to access the project
73
- if (authorization.userId !== undefined) {
74
- checks.push(
75
- authorizeTrpc.check.query({
76
- subjectSet: {
77
- namespace: "User",
78
- id: authorization.userId,
79
- },
80
- namespace,
81
- id: props.projectId,
82
- permit: props.permit,
83
- })
80
+ if (wpaRows.data.length > 0) {
81
+ const relations = wpaRows.data.flatMap((r) =>
82
+ r.relation !== null ? [r.relation] : []
84
83
  );
84
+ return { allowed: isRolePermitted(relations, input.permit) };
85
85
  }
86
86
 
87
- // Check if the special link with a token allows to access the project
88
- // Token doesn't have own permit, do not check it
89
- if (authorization.authToken !== undefined && props.permit !== "own") {
90
- checks.push(
91
- authorizeTrpc.check.query({
92
- namespace,
93
- id: props.projectId,
94
- subjectSet: {
95
- id: authorization.authToken,
96
- namespace: "Token",
97
- },
98
- permit: props.permit,
99
- })
100
- );
87
+ return { allowed: false };
88
+ }
89
+
90
+ if (input.permit === "own") {
91
+ return { allowed: false };
92
+ }
93
+
94
+ if (subjectSet.namespace !== "Token") {
95
+ return { allowed: false };
96
+ }
97
+
98
+ const row = await postgrestClient
99
+ .from("AuthorizationToken")
100
+ .select("token")
101
+ .eq("token", subjectSet.id)
102
+ .in("relation", [...permitToRelationRewrite[input.permit]])
103
+ .maybeSingle();
104
+
105
+ if (row.error) {
106
+ throw row.error;
107
+ }
108
+
109
+ return { allowed: row.data !== null };
110
+ };
111
+
112
+ // doesn't work in cloudflare workers
113
+ const memoizedCheck = memoize(check, {
114
+ // Short TTL so plan downgrades propagate quickly. No cache invalidation
115
+ // hook exists yet — keep this low until one is added.
116
+ maxAge: 10 * 1000,
117
+ cacheKey: ([_context, input]) => JSON.stringify(input),
118
+ });
119
+
120
+ type AuthInfo =
121
+ | {
122
+ type: "user";
123
+ userId: string;
124
+ }
125
+ | {
126
+ type: "token";
127
+ authToken: string;
101
128
  }
129
+ | {
130
+ type: "service";
131
+ };
132
+
133
+ export const checkProjectPermit = async ({
134
+ projectId,
135
+ permit,
136
+ authInfo,
137
+ postgrestClient,
138
+ }: {
139
+ projectId: string;
140
+ permit: AuthPermit;
141
+ authInfo: AuthInfo;
142
+ postgrestClient: AppContext["postgrest"]["client"];
143
+ }) => {
144
+ const checks = [];
145
+ const namespace = "Project";
146
+
147
+ if (authInfo.type === "service") {
148
+ return permit === "view";
149
+ }
150
+
151
+ // @todo Delete and use tokens
152
+ const templateIds = [
153
+ // Production
154
+ "5e086cf4-4293-471c-8eab-ddca8b5cd4db",
155
+ "94e6e1b8-c6c4-485a-9d7a-8282e11920c0",
156
+ "05954204-fcee-407e-b47f-77a38de74431",
157
+ "afc162c2-6396-41b7-a855-8fc04604a7b1",
158
+ "3f260731-825b-486a-b534-e747f0ed6106",
159
+ "400b1bde-def1-49e0-9b64-e26416d326fa",
160
+ "2e802ad7-ef32-48e6-8706-3a162785ef95",
161
+ "01f6f1d8-06f5-4a6c-a3b1-89a0448046c7",
162
+ "5b33acf4-53cf-4f03-8973-d5679772edee",
163
+ "909a139b-1f2d-415a-ac90-382fa19fa7d8",
164
+ "ef82ee51-e4d6-4a69-a4cc-7bf1dee65ed7",
165
+ "e761178f-6ac6-47f6-b881-56cc75640d73",
166
+ // Staging IDs
167
+ "c236999d-be6b-43fb-9edc-78a2ba59e56d",
168
+ "a1371dce-752c-4ccf-8ea4-88bab577fe50",
169
+ "6204396c-3f9e-4d29-8d19-ff0f76960a74",
170
+ ];
102
171
 
103
- if (checks.length === 0) {
172
+ // @todo Delete and use tokens
173
+ if (permit === "view" && templateIds.includes(projectId)) {
174
+ return true;
175
+ }
176
+
177
+ if (authInfo.type === "token") {
178
+ // Token doesn't have "own" permit, do not check it
179
+ if (permit === "own") {
104
180
  return false;
105
181
  }
106
182
 
107
- const authResults = await Promise.allSettled(checks);
183
+ checks.push(
184
+ memoizedCheck(postgrestClient, {
185
+ namespace,
186
+ id: projectId,
187
+ subjectSet: {
188
+ id: authInfo.authToken,
189
+ namespace: "Token",
190
+ },
191
+ permit: permit,
192
+ })
193
+ );
194
+ }
108
195
 
109
- for (const authResult of authResults) {
110
- if (authResult.status === "rejected") {
111
- throw new Error(`Authorization call failed ${authResult.reason}`);
112
- }
196
+ // Check if the user is allowed to access the project
197
+ if (authInfo.type === "user") {
198
+ checks.push(
199
+ memoizedCheck(postgrestClient, {
200
+ subjectSet: {
201
+ namespace: "User",
202
+ id: authInfo.userId,
203
+ },
204
+ namespace,
205
+ id: projectId,
206
+ permit: permit,
207
+ })
208
+ );
209
+ }
210
+
211
+ if (checks.length === 0) {
212
+ return false;
213
+ }
214
+
215
+ const authResults = await Promise.allSettled(checks);
216
+
217
+ for (const authResult of authResults) {
218
+ if (authResult.status === "rejected") {
219
+ throw new Error(`Authorization call failed ${authResult.reason}`);
113
220
  }
221
+ }
114
222
 
115
- const allowed = authResults.some(
116
- (authResult) =>
117
- authResult.status === "fulfilled" && authResult.value.allowed
118
- );
223
+ const allowed = authResults.some(
224
+ (authResult) =>
225
+ authResult.status === "fulfilled" && authResult.value.allowed
226
+ );
227
+
228
+ return allowed;
229
+ };
230
+
231
+ /**
232
+ * Look up the workspace owner's userId for a given project.
233
+ * Returns undefined when the project has no workspace.
234
+ */
235
+ const getWorkspaceOwnerIdForProject = async (
236
+ projectId: string,
237
+ context: Pick<AppContext, "postgrest">
238
+ ): Promise<string | undefined> => {
239
+ const project = await context.postgrest.client
240
+ .from("Project")
241
+ .select("workspaceId")
242
+ .eq("id", projectId)
243
+ .maybeSingle();
244
+
245
+ if (project.error || project.data?.workspaceId == null) {
246
+ return;
247
+ }
248
+
249
+ const workspace = await context.postgrest.client
250
+ .from("Workspace")
251
+ .select("userId")
252
+ .eq("id", project.data.workspaceId)
253
+ .eq("isDeleted", false)
254
+ .maybeSingle();
255
+
256
+ return workspace.data?.userId ?? undefined;
257
+ };
258
+
259
+ export const hasProjectPermit = async (
260
+ props: {
261
+ projectId: string;
262
+ permit: AuthPermit;
263
+ },
264
+ context: AppContext
265
+ ) => {
266
+ const { authorization } = context;
267
+
268
+ if (authorization.type === "anonymous") {
269
+ return false;
270
+ }
271
+
272
+ const authInfo: AuthInfo = authorization;
273
+
274
+ if (authInfo === undefined) {
275
+ return false;
276
+ }
277
+
278
+ const allowed = await checkProjectPermit({
279
+ projectId: props.projectId,
280
+ permit: props.permit,
281
+ authInfo,
282
+ postgrestClient: context.postgrest.client,
283
+ });
284
+
285
+ if (allowed === false) {
286
+ return false;
287
+ }
288
+
289
+ // Workspace downgrade check: when a workspace member accesses a project,
290
+ // verify the workspace owner's plan still supports workspace features.
291
+ // Direct project owners and workspace owners are not affected.
292
+ if (authorization.type === "user") {
293
+ // "own" permit is only granted to direct owners and workspace owners —
294
+ // both are unaffected by downgrade. This call is memoized.
295
+ const isOwner = await checkProjectPermit({
296
+ projectId: props.projectId,
297
+ permit: "own",
298
+ authInfo,
299
+ postgrestClient: context.postgrest.client,
300
+ });
119
301
 
120
- return allowed;
121
- } finally {
122
- const diff = Date.now() - start;
302
+ if (isOwner === false) {
303
+ // User is a workspace member — verify the owner's plan
304
+ const workspaceOwnerId = await getWorkspaceOwnerIdForProject(
305
+ props.projectId,
306
+ context
307
+ );
123
308
 
124
- // eslint-disable-next-line no-console
125
- console.log(`hasProjectPermit execution ${diff}ms`);
309
+ if (workspaceOwnerId !== undefined) {
310
+ const ownerPlan = await context.getOwnerPlanFeatures(workspaceOwnerId);
311
+ if (ownerPlan.maxWorkspaces <= 1) {
312
+ return false;
313
+ }
314
+ }
315
+ }
126
316
  }
317
+
318
+ return true;
127
319
  };
128
320
 
129
321
  /**
@@ -131,37 +323,33 @@ export const hasProjectPermit = async (
131
323
  * @todo think about caching to authorizeTrpc.check.query
132
324
  * batching check queries would help too https://github.com/ory/keto/issues/812
133
325
  */
134
- export const getProjectPermit = async <T extends AuthPermit>(
326
+ export const getProjectPermit = async (
135
327
  props: {
136
328
  projectId: string;
137
- permits: readonly T[];
329
+ permits: readonly AuthPermit[];
138
330
  },
139
331
  context: AppContext
140
- ): Promise<T | undefined> => {
141
- const start = Date.now();
142
-
143
- try {
144
- const permitToCheck = props.permits;
145
-
146
- const permits = await Promise.allSettled(
147
- permitToCheck.map((permit) =>
148
- hasProjectPermit({ projectId: props.projectId, permit }, context)
149
- )
150
- );
332
+ ): Promise<AuthPermit | undefined> => {
333
+ const permitToCheck = props.permits;
151
334
 
152
- for (const permit of permits) {
153
- if (permit.status === "rejected") {
154
- throw new Error(`Authorization call failed ${permit.reason}`);
155
- }
335
+ const permits = await Promise.allSettled(
336
+ permitToCheck.map((permit) =>
337
+ hasProjectPermit({ projectId: props.projectId, permit }, context)
338
+ )
339
+ );
156
340
 
157
- if (permit.value === true) {
158
- return permitToCheck[permits.indexOf(permit)];
159
- }
341
+ for (const permit of permits) {
342
+ if (permit.status === "rejected") {
343
+ throw new Error(`Authorization call failed ${permit.reason}`);
160
344
  }
161
- } finally {
162
- const diff = Date.now() - start;
163
345
 
164
- // eslint-disable-next-line no-console
165
- console.log(`getProjectPermit execution ${diff}ms`);
346
+ if (permit.value === true) {
347
+ return permitToCheck[permits.indexOf(permit)];
348
+ }
166
349
  }
167
350
  };
351
+
352
+ export const __testing__ = {
353
+ isRolePermitted,
354
+ getWorkspaceOwnerIdForProject,
355
+ };
@@ -0,0 +1,18 @@
1
+ export const roles = [
2
+ "viewers",
3
+ "editors",
4
+ "builders",
5
+ "administrators",
6
+ ] as const;
7
+
8
+ export type Role = (typeof roles)[number];
9
+
10
+ /** Safest default when role is unknown — principle of least privilege */
11
+ export const defaultRole: Role = "viewers";
12
+
13
+ export const roleLabels: Record<Role, string> = {
14
+ viewers: "Viewer",
15
+ editors: "Editor",
16
+ builders: "Builder",
17
+ administrators: "Admin",
18
+ };
@@ -1,32 +1,45 @@
1
1
  import type { TrpcInterfaceClient } from "../shared/shared-router";
2
+ import type { Client } from "@webstudio-is/postgrest/index.server";
3
+ import type { PlanFeatures, Purchase } from "@webstudio-is/plans";
2
4
 
3
5
  /**
4
6
  * All necessary parameters for Authorization
5
7
  */
6
- type AuthorizationContext = {
7
- /**
8
- * userId of the current authenticated user
9
- */
10
- userId: string | undefined;
11
-
12
- /**
13
- * token URLSearchParams or hostname
14
- */
15
- authToken: string | undefined;
16
-
17
- /**
18
- * project list serves as a template and is accessible to everyone.
19
- */
20
- projectTemplates: string[];
21
-
22
- /**
23
- * Allow service 2 service communications to skip authorization for view calls
24
- */
25
- isServiceCall: boolean;
8
+ type AuthorizationContext =
9
+ | {
10
+ type: "user";
11
+ /**
12
+ * userId of the current authenticated user
13
+ */
14
+ userId: string;
15
+ sessionCreatedAt: number;
16
+ /**
17
+ * Has projectId in the tracked sessions
18
+ */
19
+ isLoggedInToBuilder: (projectId: string) => Promise<boolean>;
20
+ }
21
+ | {
22
+ type: "token";
23
+ /**
24
+ * token URLSearchParams or hostname
25
+ */
26
+ authToken: string;
26
27
 
27
- // Pass trpcClient through context as only main app can initialize it
28
- authorizeTrpc: TrpcInterfaceClient["authorize"];
29
- };
28
+ /**
29
+ * In case of authToken, this is the ownerId of the project
30
+ */
31
+ ownerId: string;
32
+ }
33
+ | {
34
+ type: "service";
35
+ /**
36
+ * Allow service 2 service communications to skip authorization for view calls
37
+ */
38
+ isServiceCall: boolean;
39
+ }
40
+ | {
41
+ type: "anonymous";
42
+ };
30
43
 
31
44
  type DomainContext = {
32
45
  domainTrpc: TrpcInterfaceClient["domain"];
@@ -46,10 +59,21 @@ type DeploymentContext = {
46
59
  deploymentTrpc: TrpcInterfaceClient["deployment"];
47
60
  env: {
48
61
  BUILDER_ORIGIN: string;
49
- BRANCH_NAME: string;
62
+ GITHUB_REF_NAME: string;
63
+ GITHUB_SHA: string | undefined;
64
+ PUBLISHER_HOST: string;
50
65
  };
51
66
  };
52
67
 
68
+ type TrpcCache = {
69
+ setMaxAge: (path: string, value: number) => void;
70
+ getMaxAge: (path: string) => number | undefined;
71
+ };
72
+
73
+ type PostgrestContext = {
74
+ client: Client;
75
+ };
76
+
53
77
  /**
54
78
  * AppContext is a global context that is passed to all trpc/api queries/mutations
55
79
  * "authorization" is made inside the namespace because eventually there will be
@@ -60,4 +84,15 @@ export type AppContext = {
60
84
  domain: DomainContext;
61
85
  deployment: DeploymentContext;
62
86
  entri: EntriContext;
87
+ planFeatures: PlanFeatures;
88
+ purchases: Array<Purchase>;
89
+ trpcCache: TrpcCache;
90
+ postgrest: PostgrestContext;
91
+ createTokenContext: (token: string) => Promise<AppContext>;
92
+ /**
93
+ * Resolves plan features for a given user ID.
94
+ * Used by workspace authorization to check whether the workspace owner's plan
95
+ * still supports workspace features (maxWorkspaces > 1) after a potential downgrade.
96
+ */
97
+ getOwnerPlanFeatures: (userId: string) => Promise<PlanFeatures>;
63
98
  };
@@ -5,3 +5,19 @@ export const AuthorizationError = customErrorFactory(
5
5
  this.message = message;
6
6
  }
7
7
  );
8
+
9
+ /**
10
+ * Standard response for any client-server communication with an error.
11
+ */
12
+ export const createErrorResponse = (error: unknown) => {
13
+ const message =
14
+ typeof error === "string"
15
+ ? error
16
+ : error && typeof error === "object" && "message" in error
17
+ ? String(error.message)
18
+ : "Unknown error";
19
+ return {
20
+ success: false as const,
21
+ error: message,
22
+ };
23
+ };
@@ -0,0 +1,19 @@
1
+ import { initTRPC } from "@trpc/server";
2
+ import type { AppContext } from "./context.server";
3
+
4
+ export const {
5
+ router,
6
+ procedure,
7
+ middleware,
8
+ mergeRouters,
9
+ createCallerFactory,
10
+ } = initTRPC.context<AppContext>().create();
11
+
12
+ export const createCacheMiddleware = (seconds: number) =>
13
+ middleware(async ({ path, ctx, next }) => {
14
+ // tRPC batches multiple requests into a single network call.
15
+ // The `path` is used as key to find the least max age among all paths for caching
16
+ ctx.trpcCache.setMaxAge(path, seconds);
17
+
18
+ return next({ ctx });
19
+ });
@@ -2,7 +2,19 @@ export type { SharedRouter } from "./shared/shared-router";
2
2
  export { createTrpcProxyServiceClient } from "./shared/client";
3
3
 
4
4
  export type { AppContext } from "./context/context.server";
5
- export { AuthorizationError } from "./context/errors.server";
5
+
6
+ export {
7
+ AuthorizationError,
8
+ createErrorResponse,
9
+ } from "./context/errors.server";
6
10
  export * as authorizeProject from "./authorize/project.server";
7
- export * as authorizeAuthorizationToken from "./authorize/authorization-token.server";
8
- export type { AuthPermit } from "./shared/authorization-router";
11
+ export type { AuthPermit } from "./authorize/project.server";
12
+
13
+ export {
14
+ router,
15
+ procedure,
16
+ middleware,
17
+ mergeRouters,
18
+ createCacheMiddleware,
19
+ createCallerFactory,
20
+ } from "./context/router.server";
@@ -5,7 +5,6 @@ import {
5
5
  type SharedRouter,
6
6
  } from "./shared-router";
7
7
  import { callerLink } from "../trpc-caller-link";
8
- import fetch from "node-fetch";
9
8
 
10
9
  type SharedClientOptions = {
11
10
  url: string;
@@ -21,7 +20,6 @@ export const createTrpcProxyServiceClient = (
21
20
  links: [
22
21
  httpBatchLink({
23
22
  url: options.url,
24
- fetch: fetch as never,
25
23
  headers: () => ({
26
24
  Authorization: options.token,
27
25
  // We use this header for SaaS preview service discovery proxy