@protobase/server 0.31.2 → 0.33.0

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.
@@ -0,0 +1,36 @@
1
+ import { authBasePath } from "./auth-base-path.js";
2
+ //#region src/better-auth/without-session-tokens.ts
3
+ const apiTokenPath = `${authBasePath}/token`;
4
+ const withoutTokens = (value) => {
5
+ if (Array.isArray(value)) return value.map(withoutTokens);
6
+ if (value === null || typeof value !== "object") return value;
7
+ return Object.fromEntries(Object.entries(value).filter(([key, field]) => !(key === "token" && typeof field === "string")).map(([key, field]) => [key, withoutTokens(field)]));
8
+ };
9
+ const parsed = (text) => {
10
+ try {
11
+ return JSON.parse(text);
12
+ } catch (error) {
13
+ if (error instanceof SyntaxError) throw new Error("Better Auth answered malformed JSON");
14
+ throw error;
15
+ }
16
+ };
17
+ /**
18
+ * Better Auth's answer to the page without the session tokens in its JSON, such as `get-session`'s `session.token` or a
19
+ * sign-in's `token`: the session is the httpOnly cookie's alone, so script on the page never holds a long-lived credential.
20
+ */
21
+ const withoutSessionTokens = async (request, response) => {
22
+ if (new URL(request.url).pathname === apiTokenPath) return response;
23
+ if (response.body === null) return response;
24
+ if (!response.headers.get("content-type")?.toLowerCase().includes("application/json")) return response;
25
+ const text = await response.text();
26
+ const headers = new Headers(response.headers);
27
+ headers.delete("content-length");
28
+ const body = text ? JSON.stringify(withoutTokens(parsed(text))) : null;
29
+ return new Response(body, {
30
+ status: response.status,
31
+ statusText: response.statusText,
32
+ headers
33
+ });
34
+ };
35
+ //#endregion
36
+ export { withoutSessionTokens };
@@ -22,6 +22,7 @@ import { readAppName } from "./app-name.js";
22
22
  import { realtimePath, restPath, storagePath, supabaseFunctionsPath, supabasePath } from "./supabase-paths.js";
23
23
  import { goTruePath } from "./gotrue/keys.js";
24
24
  import { authBasePath } from "./better-auth/auth-base-path.js";
25
+ import { withoutSessionTokens } from "./better-auth/without-session-tokens.js";
25
26
  import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
26
27
  import { statusRoute } from "./better-auth/status-route.js";
27
28
  import { goTrueTokens } from "./gotrue/access-tokens.js";
@@ -253,7 +254,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
253
254
  registry,
254
255
  db
255
256
  }));
256
- app.on(["GET", "POST"], `${authBasePath}/*`, (c) => auth.handler(c.req.raw));
257
+ app.on(["GET", "POST"], `${authBasePath}/*`, async (c) => withoutSessionTokens(c.req.raw, await auth.handler(c.req.raw)));
257
258
  app.route(goTruePath, goTrueRoutes(auth));
258
259
  if (auth.origins.length > 1) app.get(webauthnPath, (c) => c.json({ origins: auth.origins }));
259
260
  }
@@ -47,6 +47,11 @@ export type CorsOptions = {
47
47
  type SharedOptions = {
48
48
  /** Answers CORS preflights before authentication and adds the CORS headers to every response, problems included. */
49
49
  cors?: CorsOptions;
50
+ /**
51
+ * Also answers at `/api/sb/functions/v1/<name>`, as Supabase's `verify_jwt` does, where the app's publishable key alone
52
+ * admits a caller. For functions moved from Supabase; not together with `roles` or `capability`.
53
+ */
54
+ supabase?: boolean;
50
55
  };
51
56
  /** A function anyone may call, such as a webhook; it can still ask for a caller with `context.caller()`. */
52
57
  export type PublicFunctionOptions = SharedOptions & {
@@ -78,7 +83,7 @@ export type FunctionSource = ApiFunction | FunctionHandler | FunctionApp;
78
83
  /**
79
84
  * A function of your own, served at `/api/functions/<name>`. Without options it needs a signed-in caller, through the
80
85
  * app's `authenticate`, and with the app's `roles` one whose roles hold `*`; `{ roles }` or `{ capability }` opens it
81
- * to more, `{ public: true }` to anyone, as for a webhook.
86
+ * to more, `{ public: true }` to anyone, as for a webhook. `{ supabase: true }` serves it at `/api/sb/functions/v1/<name>` too.
82
87
  */
83
88
  export declare function defineFunction(handler: FunctionHandler | FunctionApp): ApiFunction;
84
89
  export declare function defineFunction(options: PublicFunctionOptions, handler: PublicFunctionHandler | FunctionApp<PublicFunctionContext>): ApiFunction;
@@ -7,11 +7,12 @@ import { Hono } from "hono";
7
7
  import { cors } from "hono/cors";
8
8
  //#region src/functions/supabase-routes.ts
9
9
  /**
10
- * The app's functions again at `/api/sb/functions/v1/<name>`, as Supabase serves Edge Functions. A protected function lets in
11
- * a request its app's keys admit, as Supabase's `verify_jwt` does, and a public one anyone. Either gets the key-made
12
- * caller, not the app's session, so a function with `roles` is served at `/api/functions/<name>` only. As on Supabase,
13
- * the function sees the path from its name on, `/<name>/more/parts`, and a CORS preflight, which carries no key,
14
- * reaches it unchecked.
10
+ * The functions with `supabase: true` again at `/api/sb/functions/v1/<name>`, as Supabase serves Edge Functions. A
11
+ * protected one lets in a request its app's keys admit, as Supabase's `verify_jwt` does, the publishable key alone
12
+ * included, and a public one anyone. Either gets the key-made caller, not the app's session, so neither may have
13
+ * `roles` or `capability`: that stops startup. As on Supabase, the function sees the path from its name on,
14
+ * `/<name>/more/parts`, and a CORS preflight, which carries no key, reaches it unchecked. Every other function is a 404
15
+ * here, and none of its code runs.
15
16
  */
16
17
  const supabaseFunctionRoutes = ({ functions, db, keys, caller, onError }) => {
17
18
  const app = new Hono().basePath(supabaseFunctionsPath);
@@ -19,9 +20,17 @@ const supabaseFunctionRoutes = ({ functions, db, keys, caller, onError }) => {
19
20
  const hasKeys = keys.publishable !== void 0 || keys.secret !== void 0;
20
21
  for (const [name, fn] of Object.entries(functions)) {
21
22
  const { options } = fn;
23
+ if (options.supabase !== true) {
24
+ app.all(`/${name}/*`, () => {
25
+ throw notFound(`The function "${name}" answers at /api/functions/${name} only: give it supabase: true to answer here too`);
26
+ });
27
+ continue;
28
+ }
29
+ const { roles, capability } = options;
30
+ const gated = roles !== void 0 ? "roles" : capability !== void 0 ? "a capability" : void 0;
31
+ if (gated) throw new Error(`The function "${name}" has supabase: true and ${gated}, which /api/sb/functions/v1 cannot check: drop one`);
22
32
  if (options.cors) app.use(`/${name}/*`, cors(options.cors));
23
33
  app.all(`/${name}/*`, (c) => runInBackground(async () => {
24
- if (!options.public && options.roles) throw notFound(`The function "${name}" checks roles, so it is served at /api/functions/${name} only`);
25
34
  const request = relativeRequest(c.req.raw, supabaseFunctionsPath);
26
35
  if (options.public || c.req.method === "OPTIONS") {
27
36
  let resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.31.2",
3
+ "version": "0.33.0",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.7",
34
34
  "@better-auth/sso": "^1.7.7",
35
- "@protobase/layout": "0.31.2",
36
- "@protobase/query": "0.31.2",
37
- "@protobase/schema": "0.31.2",
35
+ "@protobase/layout": "0.33.0",
36
+ "@protobase/query": "0.33.0",
37
+ "@protobase/schema": "0.33.0",
38
38
  "@react-email/render": "^2.1.0",
39
39
  "@scalar/hono-api-reference": "^0.12.10",
40
40
  "@sentry/core": "^11.7.0",