cloudflare-next-intl 0.7.7 → 0.8.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,125 @@
1
+ import config from '../config/intl_config';
2
+ import requireDbConfig from './require_config';
3
+ import connectToPostgres, { disconnectPostgres } from './connection';
4
+ import resolveDbMode from './resolve_mode';
5
+ import resolveSupabaseEndpoint from './supabase_config';
6
+ import createSupabaseTransport from './supabase_transport';
7
+ import resolveAccessToken from './access_token';
8
+ const DEFAULT_ROLE = 'authenticated';
9
+ /**
10
+ * Resolves the user id for `withUserDb`, trying, in order: the explicit `uid`
11
+ * argument, `db.getUserId()`, then the signed-in Firebase user.
12
+ */
13
+ async function resolveUserId(uid) {
14
+ if (uid)
15
+ return uid;
16
+ const db = config.db;
17
+ requireDbConfig(db);
18
+ const fromConfig = await db.getUserId?.();
19
+ if (fromConfig)
20
+ return fromConfig;
21
+ if (config.firebaseAuth) {
22
+ const { getAuthUser } = await import('../firebase_auth/server/use_auth_user_server');
23
+ const { user } = await getAuthUser();
24
+ if (user?.uid)
25
+ return user.uid;
26
+ }
27
+ throw new Error('db: withUserDb could not resolve a user id. Pass one explicitly, set ' +
28
+ '`db.getUserId`, or configure `firebaseAuth` so the signed-in Firebase uid is used.');
29
+ }
30
+ /**
31
+ * Builds a Drizzle handle backed by PostgREST. `bearerToken` decides the role
32
+ * Postgres sees: the anon key for public access, a user JWT for `withUserDb`.
33
+ */
34
+ async function supabaseDb(supabase, bearerToken) {
35
+ const { drizzle } = await import('drizzle-orm/pg-proxy');
36
+ return drizzle(createSupabaseTransport(supabase, bearerToken));
37
+ }
38
+ /**
39
+ * Runs a query as the **anonymous** role: no transaction, no role switch, no
40
+ * user identity attached. Use this for data any visitor may read.
41
+ *
42
+ * Because no user id is set, RLS policies that test `auth.jwt()->>'sub'` see
43
+ * no user and will deny access — reach for {@link withUserDb} whenever the
44
+ * rows depend on who is asking.
45
+ *
46
+ * In connection-string mode the connection is taken from the request's
47
+ * shared client and released when `fn` settles, even if it throws. In
48
+ * Supabase mode there is no connection to release — each call is one
49
+ * PostgREST round-trip authenticated as the anon key.
50
+ *
51
+ * @param fn Receives the Drizzle handle; return whatever the caller needs.
52
+ * @returns Whatever `fn` resolves to.
53
+ * @throws If `db` is not set on your `RoutingConfig`, or the connection fails.
54
+ *
55
+ * @example
56
+ * const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
57
+ */
58
+ export async function withPublicDb(fn) {
59
+ const db = config.db;
60
+ requireDbConfig(db);
61
+ if (resolveDbMode(db) === 'supabase') {
62
+ const supabase = db.supabase;
63
+ const { anonKey } = resolveSupabaseEndpoint(supabase);
64
+ return fn(await supabaseDb(supabase, anonKey));
65
+ }
66
+ const client = await connectToPostgres(config);
67
+ try {
68
+ const { drizzle } = await import('drizzle-orm/node-postgres');
69
+ return await fn(drizzle(client));
70
+ }
71
+ finally {
72
+ disconnectPostgres(config);
73
+ }
74
+ }
75
+ /**
76
+ * Runs a query as the **signed-in user**.
77
+ *
78
+ * In connection-string mode this runs inside a transaction where Postgres
79
+ * sees the resolved user id as `auth.jwt()->>'sub'` under
80
+ * `db.authenticatedRole`, so RLS policies behave exactly as they do for a
81
+ * PostgREST-issued call. In Supabase mode identity instead rides on the JWT
82
+ * sent as `Authorization: Bearer` — PostgREST resolves the `authenticated`
83
+ * role and populates `request.jwt.claims` itself, and each statement is its
84
+ * own round-trip with no cross-statement transaction (the Postgres proxy
85
+ * Drizzle uses in this mode cannot open one). Either way this is the wrapper
86
+ * to use for anything user-owned.
87
+ *
88
+ * @param fn Receives the Drizzle handle. In connection-string mode it is
89
+ * bound to a transaction; in Supabase mode it is not — do not rely on
90
+ * multi-statement atomicity there.
91
+ * @param uid Connection-string mode only: overrides the user id. Omit it in
92
+ * normal use — the id then comes from `db.getUserId()` when set, otherwise
93
+ * from the signed-in Firebase user when `firebaseAuth` is configured.
94
+ * Ignored in Supabase mode, which resolves identity via `db.getAccessToken`/
95
+ * Firebase instead — see {@link resolveAccessToken}.
96
+ * @returns Whatever `fn` resolves to.
97
+ * @throws If `db` is not set on your `RoutingConfig`, if no user id/access
98
+ * token can be resolved, or the connection fails.
99
+ *
100
+ * @example
101
+ * const mine = await withUserDb((db) => db.select().from(orders));
102
+ */
103
+ export async function withUserDb(fn, uid) {
104
+ const db = config.db;
105
+ requireDbConfig(db);
106
+ if (resolveDbMode(db) === 'supabase') {
107
+ const token = await resolveAccessToken(config);
108
+ return fn(await supabaseDb(db.supabase, token));
109
+ }
110
+ const userId = await resolveUserId(uid);
111
+ const client = await connectToPostgres(config);
112
+ const role = db.authenticatedRole ?? DEFAULT_ROLE;
113
+ try {
114
+ const { drizzle } = await import('drizzle-orm/node-postgres');
115
+ const { sql } = await import('drizzle-orm');
116
+ return await drizzle(client).transaction(async (transaction) => {
117
+ await transaction.execute(sql `select set_config('request.jwt.claims', ${JSON.stringify({ sub: userId })}, true)`);
118
+ await transaction.execute(sql `set local role ${sql.raw(role)}`);
119
+ return fn(transaction);
120
+ });
121
+ }
122
+ finally {
123
+ disconnectPostgres(config);
124
+ }
125
+ }
@@ -0,0 +1,57 @@
1
+ import { type SQL, type Table } from 'drizzle-orm';
2
+ /**
3
+ * Type-safe helper returning `excluded.<db_column_name>` SQL expressions for a Drizzle table.
4
+ *
5
+ * Restricts property access at compile time to valid table schema keys (camelCase),
6
+ * and validates at runtime by throwing a descriptive error if an unknown property is accessed.
7
+ *
8
+ * @example
9
+ * set: {
10
+ * inflation: excluded(inflation).inflation,
11
+ * yearInflation: excluded(inflation).yearInflation,
12
+ * updatedAt: sql`now()`,
13
+ * }
14
+ */
15
+ export declare function excluded<T extends Table>(table: T): {
16
+ [K in keyof T["_"]["columns"]]: SQL;
17
+ };
18
+ /**
19
+ * Builds a runtime and compile-time validated `set` object for `onConflictDoUpdate`.
20
+ *
21
+ * Only permits valid column keys of the target table. Automatically maps
22
+ * property keys to database column names and appends `updatedAt: sql\`now()\``
23
+ * if the column exists on the table.
24
+ *
25
+ * @example
26
+ * .onConflictDoUpdate({
27
+ * target: inflation.date,
28
+ * set: onConflictSet(inflation, ["inflation", "yearInflation"]),
29
+ * })
30
+ */
31
+ export declare function onConflictSet<T extends Table, K extends keyof T["_"]["columns"]>(table: T, fields: K[]): Record<string, SQL>;
32
+ export type TimeUnit = "days" | "hours" | "minutes" | "months" | "years" | "weeks";
33
+ /** General SQL helper generating a timestamp expression relative to now (`now() - (N unit)::interval`). */
34
+ export declare function ago(amount: number, unit: TimeUnit): SQL;
35
+ /** General SQL helper returning `current_date`. */
36
+ export declare function currentDate(): SQL;
37
+ /** General SQL helper for window function `count(*) over ()`. */
38
+ export declare function windowCount(): SQL<number>;
39
+ /** Helper generating `lateral unnest(...) as alias(alias)`. */
40
+ export declare function unnestLateral(column: unknown, alias: string): SQL;
41
+ /** Helper generating `column asc nulls last`. */
42
+ export declare function ascNullsLast(column: unknown): SQL;
43
+ /** General SQL literal `true`, for lateral joins and empty predicate lists. */
44
+ export declare function alwaysTrue(): SQL;
45
+ /** Wraps an expression as `lateral (<inner>) <alias>`. */
46
+ export declare function lateral(inner: SQL, alias: string): SQL;
47
+ /** References `<alias>.<column>` of a derived table / lateral subquery. */
48
+ export declare function aliasColumn<T = unknown>(alias: string, column: string): SQL<T>;
49
+ /** General aggregate helpers over an arbitrary expression. */
50
+ export declare function minOf<T = unknown>(expression: unknown): SQL<T>;
51
+ export declare function maxOf<T = unknown>(expression: unknown): SQL<T>;
52
+ /** Rounds an expression to `digits` decimals, returning `real`. */
53
+ export declare function roundReal(expression: unknown, digits: number): SQL<number>;
54
+ /** Multiplies an expression by a factor. */
55
+ export declare function multiply(expression: unknown, factor: number): SQL<number>;
56
+ /** Wraps an expression as a scalar subquery over a named CTE: `(select <expr> from <cte>)`. */
57
+ export declare function scalarFromCte<T = unknown>(cte: string, expression: unknown): SQL<T>;
@@ -0,0 +1,112 @@
1
+ import { getTableColumns, getTableName, sql } from 'drizzle-orm';
2
+ /**
3
+ * Type-safe helper returning `excluded.<db_column_name>` SQL expressions for a Drizzle table.
4
+ *
5
+ * Restricts property access at compile time to valid table schema keys (camelCase),
6
+ * and validates at runtime by throwing a descriptive error if an unknown property is accessed.
7
+ *
8
+ * @example
9
+ * set: {
10
+ * inflation: excluded(inflation).inflation,
11
+ * yearInflation: excluded(inflation).yearInflation,
12
+ * updatedAt: sql`now()`,
13
+ * }
14
+ */
15
+ export function excluded(table) {
16
+ const cols = getTableColumns(table);
17
+ const tableName = getTableName(table);
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ return new Proxy({}, {
20
+ get(_, prop) {
21
+ const col = cols[prop];
22
+ if (!col) {
23
+ throw new Error(`[Drizzle Error] Column "${prop}" does not exist on table "${tableName}". ` +
24
+ `Valid columns: ${Object.keys(cols).join(", ")}`);
25
+ }
26
+ return sql.raw(`excluded.${col.name}`);
27
+ },
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ });
30
+ }
31
+ /**
32
+ * Builds a runtime and compile-time validated `set` object for `onConflictDoUpdate`.
33
+ *
34
+ * Only permits valid column keys of the target table. Automatically maps
35
+ * property keys to database column names and appends `updatedAt: sql\`now()\``
36
+ * if the column exists on the table.
37
+ *
38
+ * @example
39
+ * .onConflictDoUpdate({
40
+ * target: inflation.date,
41
+ * set: onConflictSet(inflation, ["inflation", "yearInflation"]),
42
+ * })
43
+ */
44
+ export function onConflictSet(table, fields) {
45
+ const cols = getTableColumns(table);
46
+ const tableName = getTableName(table);
47
+ const setObj = {};
48
+ for (const field of fields) {
49
+ const key = String(field);
50
+ const col = cols[key];
51
+ if (!col) {
52
+ throw new Error(`[Drizzle Error] Column "${key}" does not exist on table "${tableName}". ` +
53
+ `Valid columns: ${Object.keys(cols).join(", ")}`);
54
+ }
55
+ setObj[key] = sql.raw(`excluded.${col.name}`);
56
+ }
57
+ if ("updatedAt" in cols) {
58
+ setObj.updatedAt = sql `now()`;
59
+ }
60
+ return setObj;
61
+ }
62
+ /** General SQL helper generating a timestamp expression relative to now (`now() - (N unit)::interval`). */
63
+ export function ago(amount, unit) {
64
+ return sql `now() - (${amount} || ' ' || ${unit})::interval`;
65
+ }
66
+ /** General SQL helper returning `current_date`. */
67
+ export function currentDate() {
68
+ return sql `current_date`;
69
+ }
70
+ /** General SQL helper for window function `count(*) over ()`. */
71
+ export function windowCount() {
72
+ return sql `count(*) over ()`.mapWith(Number);
73
+ }
74
+ /** Helper generating `lateral unnest(...) as alias(alias)`. */
75
+ export function unnestLateral(column, alias) {
76
+ return sql `lateral unnest(${column}) as ${sql.raw(alias)}(${sql.raw(alias)})`;
77
+ }
78
+ /** Helper generating `column asc nulls last`. */
79
+ export function ascNullsLast(column) {
80
+ return sql `${column} asc nulls last`;
81
+ }
82
+ /** General SQL literal `true`, for lateral joins and empty predicate lists. */
83
+ export function alwaysTrue() {
84
+ return sql `true`;
85
+ }
86
+ /** Wraps an expression as `lateral (<inner>) <alias>`. */
87
+ export function lateral(inner, alias) {
88
+ return sql `lateral (${inner}) ${sql.raw(alias)}`;
89
+ }
90
+ /** References `<alias>.<column>` of a derived table / lateral subquery. */
91
+ export function aliasColumn(alias, column) {
92
+ return sql.raw(`${alias}.${column}`);
93
+ }
94
+ /** General aggregate helpers over an arbitrary expression. */
95
+ export function minOf(expression) {
96
+ return sql `min(${expression})`;
97
+ }
98
+ export function maxOf(expression) {
99
+ return sql `max(${expression})`;
100
+ }
101
+ /** Rounds an expression to `digits` decimals, returning `real`. */
102
+ export function roundReal(expression, digits) {
103
+ return sql `round((${expression})::numeric, ${digits})::real`;
104
+ }
105
+ /** Multiplies an expression by a factor. */
106
+ export function multiply(expression, factor) {
107
+ return sql `${expression} * ${factor}`;
108
+ }
109
+ /** Wraps an expression as a scalar subquery over a named CTE: `(select <expr> from <cte>)`. */
110
+ export function scalarFromCte(cte, expression) {
111
+ return sql `(select ${expression} from ${sql.raw(cte)})`;
112
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Optional Postgres/Drizzle data-access layer, reached from
3
+ * `cloudflare-next-intl/db`. Enable it by setting `db` on your `RoutingConfig`;
4
+ * every export here throws a descriptive error if that config is missing.
5
+ *
6
+ * Pick a wrapper by who is allowed to see the rows:
7
+ * - {@link withPublicDb} — anonymous role, for data any visitor may read.
8
+ * - {@link withUserDb} — the signed-in user, with RLS applied to their id.
9
+ *
10
+ * Two transports reach Postgres behind that same Drizzle query API, chosen by
11
+ * `resolveDbMode` from which `db` config fields are set: `connectionString`/
12
+ * `hyperdriveBinding` for a direct connection (wins if both are configured),
13
+ * or `supabase` for the Supabase Data API when only a project URL and anon
14
+ * key are available. `pg`, `drizzle-orm`, and `@supabase/supabase-js` all
15
+ * load through dynamic `import()` inside these functions, so an app that
16
+ * never calls a `db` export never bundles any of them.
17
+ *
18
+ * Generic Drizzle SQL helpers (`excluded`, `onConflictSet`, `ago`, …) live in
19
+ * the separate `cloudflare-next-intl/dbHelpers` entry point.
20
+ */
21
+ export { withPublicDb, withUserDb } from './context';
22
+ export type { DrizzleDb } from './context';
23
+ export { default as connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
24
+ export type { DbRoutingConfig } from '../types/types';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Optional Postgres/Drizzle data-access layer, reached from
3
+ * `cloudflare-next-intl/db`. Enable it by setting `db` on your `RoutingConfig`;
4
+ * every export here throws a descriptive error if that config is missing.
5
+ *
6
+ * Pick a wrapper by who is allowed to see the rows:
7
+ * - {@link withPublicDb} — anonymous role, for data any visitor may read.
8
+ * - {@link withUserDb} — the signed-in user, with RLS applied to their id.
9
+ *
10
+ * Two transports reach Postgres behind that same Drizzle query API, chosen by
11
+ * `resolveDbMode` from which `db` config fields are set: `connectionString`/
12
+ * `hyperdriveBinding` for a direct connection (wins if both are configured),
13
+ * or `supabase` for the Supabase Data API when only a project URL and anon
14
+ * key are available. `pg`, `drizzle-orm`, and `@supabase/supabase-js` all
15
+ * load through dynamic `import()` inside these functions, so an app that
16
+ * never calls a `db` export never bundles any of them.
17
+ *
18
+ * Generic Drizzle SQL helpers (`excluded`, `onConflictSet`, `ago`, …) live in
19
+ * the separate `cloudflare-next-intl/dbHelpers` entry point.
20
+ */
21
+ export { withPublicDb, withUserDb } from './context';
22
+ export { default as connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
@@ -0,0 +1,15 @@
1
+ import type { DbRoutingConfig } from '../types/types';
2
+ /**
3
+ * Asserts that the optional `db` config is present, narrowing it from
4
+ * `DbRoutingConfig | undefined` to `DbRoutingConfig` for the rest of the
5
+ * caller's scope.
6
+ *
7
+ * Every `db` export calls this before touching `pg`/`drizzle-orm`. It throws
8
+ * rather than silently no-op'ing, so a consumer who calls e.g. `withPublicDb()`
9
+ * without setting `db` on their `RoutingConfig` gets an immediate, actionable
10
+ * error instead of a confusing failed query.
11
+ *
12
+ * @param db The `db` field off your routing config.
13
+ * @throws If `db` is undefined.
14
+ */
15
+ export default function requireDbConfig(db: DbRoutingConfig | undefined): asserts db is DbRoutingConfig;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Asserts that the optional `db` config is present, narrowing it from
3
+ * `DbRoutingConfig | undefined` to `DbRoutingConfig` for the rest of the
4
+ * caller's scope.
5
+ *
6
+ * Every `db` export calls this before touching `pg`/`drizzle-orm`. It throws
7
+ * rather than silently no-op'ing, so a consumer who calls e.g. `withPublicDb()`
8
+ * without setting `db` on their `RoutingConfig` gets an immediate, actionable
9
+ * error instead of a confusing failed query.
10
+ *
11
+ * @param db The `db` field off your routing config.
12
+ * @throws If `db` is undefined.
13
+ */
14
+ export default function requireDbConfig(db) {
15
+ if (!db) {
16
+ throw new Error('db: `db` is not set on your RoutingConfig. Add a `db` object ' +
17
+ '(connectionString or hyperdriveBinding) to the config passed to ' +
18
+ '`setIntlConfig` before using any db export.');
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { DbRoutingConfig } from '../types/types';
2
+ /** Which transport the `db` exports use for a given config. */
3
+ export type DbMode = 'postgres' | 'supabase';
4
+ /**
5
+ * Decides how to reach the database from the shape of the `db` config.
6
+ *
7
+ * Direct Postgres wins whenever it is configured, so adding a `supabase`
8
+ * block to an existing config never silently reroutes live traffic. With
9
+ * neither set the result is still `'postgres'`, which lets
10
+ * `connectToPostgres` raise its existing, more specific error about the
11
+ * missing Hyperdrive binding.
12
+ *
13
+ * @param db The `db` field off your routing config.
14
+ * @returns `'postgres'` for connection-string/Hyperdrive access, `'supabase'`
15
+ * for PostgREST access.
16
+ */
17
+ export default function resolveDbMode(db: DbRoutingConfig): DbMode;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Decides how to reach the database from the shape of the `db` config.
3
+ *
4
+ * Direct Postgres wins whenever it is configured, so adding a `supabase`
5
+ * block to an existing config never silently reroutes live traffic. With
6
+ * neither set the result is still `'postgres'`, which lets
7
+ * `connectToPostgres` raise its existing, more specific error about the
8
+ * missing Hyperdrive binding.
9
+ *
10
+ * @param db The `db` field off your routing config.
11
+ * @returns `'postgres'` for connection-string/Hyperdrive access, `'supabase'`
12
+ * for PostgREST access.
13
+ */
14
+ export default function resolveDbMode(db) {
15
+ if (db.connectionString || db.hyperdriveBinding)
16
+ return 'postgres';
17
+ return db.supabase ? 'supabase' : 'postgres';
18
+ }
@@ -0,0 +1,18 @@
1
+ import type { SupabaseDbConfig } from '../types/types';
2
+ /** The Supabase project URL and anon key the transport builds a client from. */
3
+ export interface ResolvedSupabaseEndpoint {
4
+ /** Project URL, trailing slashes stripped. */
5
+ url: string;
6
+ /** Anon key, sent as both `apikey` and the public-mode bearer token. */
7
+ anonKey: string;
8
+ }
9
+ /**
10
+ * Resolves the Supabase project URL and anon key, preferring explicit config
11
+ * over the `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`
12
+ * environment variables.
13
+ *
14
+ * @param supabase The `db.supabase` config block.
15
+ * @returns The project URL and anon key to build a Supabase client from.
16
+ * @throws If neither config nor environment supplies a URL or an anon key.
17
+ */
18
+ export default function resolveSupabaseEndpoint(supabase: SupabaseDbConfig): ResolvedSupabaseEndpoint;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Resolves the Supabase project URL and anon key, preferring explicit config
3
+ * over the `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`
4
+ * environment variables.
5
+ *
6
+ * @param supabase The `db.supabase` config block.
7
+ * @returns The project URL and anon key to build a Supabase client from.
8
+ * @throws If neither config nor environment supplies a URL or an anon key.
9
+ */
10
+ export default function resolveSupabaseEndpoint(supabase) {
11
+ const url = supabase.url ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
12
+ if (!url) {
13
+ throw new Error('db: could not resolve a Supabase project URL. Set `db.supabase.url` ' +
14
+ 'or the NEXT_PUBLIC_SUPABASE_URL environment variable.');
15
+ }
16
+ const anonKey = supabase.anonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
17
+ if (!anonKey) {
18
+ throw new Error('db: could not resolve a Supabase anon key. Set `db.supabase.anonKey` ' +
19
+ 'or the NEXT_PUBLIC_SUPABASE_ANON_KEY environment variable.');
20
+ }
21
+ return { url: url.replace(/\/+$/, ''), anonKey };
22
+ }
@@ -0,0 +1,29 @@
1
+ import type { SupabaseDbConfig } from '../types/types';
2
+ /**
3
+ * The executor shape `drizzle-orm/pg-proxy` calls with each generated
4
+ * statement. Declared structurally so this file never imports `drizzle-orm`.
5
+ */
6
+ export type SupabaseRemoteCallback = (sql: string, params: unknown[], method: 'all' | 'execute') => Promise<{
7
+ rows: unknown[];
8
+ }>;
9
+ /**
10
+ * Builds the transport Drizzle uses in Supabase mode: every generated
11
+ * statement is sent through `@supabase/supabase-js`'s `.rpc()` to the
12
+ * `cfni_exec` function over PostgREST.
13
+ *
14
+ * `bearerToken` decides who Postgres thinks is calling — the anon key for
15
+ * public reads, a user's JWT for `withUserDb` — delivered through the
16
+ * client's `accessToken` option (the same mechanism a signed-in Supabase
17
+ * session would use), so RLS is enforced by the database rather than by
18
+ * anything in this package. The client is created once and reused for every
19
+ * statement this transport is asked to run.
20
+ *
21
+ * Rows come back as positional arrays because `pg-proxy` maps result columns
22
+ * by index; `cfni_exec` is what guarantees that shape.
23
+ *
24
+ * @param supabase The `db.supabase` config block.
25
+ * @param bearerToken Token resolved as the caller's identity — the anon key,
26
+ * or a per-request user JWT.
27
+ * @returns A callback suitable for `drizzle-orm/pg-proxy`'s `drizzle()`.
28
+ */
29
+ export default function createSupabaseTransport(supabase: SupabaseDbConfig, bearerToken: string): SupabaseRemoteCallback;
@@ -0,0 +1,49 @@
1
+ import resolveSupabaseEndpoint from './supabase_config';
2
+ const DEFAULT_EXEC_FUNCTION = 'cfni_exec';
3
+ /**
4
+ * Builds the transport Drizzle uses in Supabase mode: every generated
5
+ * statement is sent through `@supabase/supabase-js`'s `.rpc()` to the
6
+ * `cfni_exec` function over PostgREST.
7
+ *
8
+ * `bearerToken` decides who Postgres thinks is calling — the anon key for
9
+ * public reads, a user's JWT for `withUserDb` — delivered through the
10
+ * client's `accessToken` option (the same mechanism a signed-in Supabase
11
+ * session would use), so RLS is enforced by the database rather than by
12
+ * anything in this package. The client is created once and reused for every
13
+ * statement this transport is asked to run.
14
+ *
15
+ * Rows come back as positional arrays because `pg-proxy` maps result columns
16
+ * by index; `cfni_exec` is what guarantees that shape.
17
+ *
18
+ * @param supabase The `db.supabase` config block.
19
+ * @param bearerToken Token resolved as the caller's identity — the anon key,
20
+ * or a per-request user JWT.
21
+ * @returns A callback suitable for `drizzle-orm/pg-proxy`'s `drizzle()`.
22
+ */
23
+ export default function createSupabaseTransport(supabase, bearerToken) {
24
+ const { url, anonKey } = resolveSupabaseEndpoint(supabase);
25
+ const execFunction = supabase.execFunction ?? DEFAULT_EXEC_FUNCTION;
26
+ let clientPromise = null;
27
+ async function getClient() {
28
+ clientPromise ?? (clientPromise = (async () => {
29
+ const { createClient } = await import('@supabase/supabase-js');
30
+ return createClient(url, anonKey, { accessToken: async () => bearerToken });
31
+ })());
32
+ return clientPromise;
33
+ }
34
+ return async (sql, params) => {
35
+ const client = await getClient();
36
+ const { data, error } = await client.rpc(execFunction, { statement: sql, params });
37
+ if (error)
38
+ throw new Error(describeFailure(error, execFunction));
39
+ return { rows: Array.isArray(data) ? data : [] };
40
+ };
41
+ }
42
+ function describeFailure(error, execFunction) {
43
+ // PGRST202 is PostgREST's "no such function" — by far the most likely
44
+ // first-run failure, so point at the install step instead of the raw code.
45
+ if (error.code === 'PGRST202') {
46
+ return `db: Supabase rejected the query — ${error.message}. Install the ${execFunction} function from supabase/cfni_exec.sql in your database.`;
47
+ }
48
+ return `db: Supabase rejected the query — ${error.message}.`;
49
+ }
@@ -3,20 +3,28 @@ import type { FirebaseAppCheckConfig } from '../../types/types';
3
3
  * Mints a fresh App Check token server-side via a service account, for use
4
4
  * when the client-written App Check cookie (see `appCheckTokenCookieName`)
5
5
  * is absent — e.g. a cold navigation before `AuthUserProvider` has run and
6
- * had a chance to write it. Requires `clientEmail`/`privateKey`/`appId` on
7
- * `firebaseAuth.appCheck`; returns `undefined` (never throws) if the
8
- * exchange fails, so a caller can always fall back to "no App Check token"
9
- * exactly as before this existed.
6
+ * had a chance to write it. Requires `clientEmail`/`appId` on
7
+ * `firebaseAuth.appCheck`, plus either `privateKey` or the
8
+ * `oauthClientId`/`oauthClientSecret`/`oauthRefreshToken` triple; returns
9
+ * `undefined` (never throws) if the exchange fails, so a caller can always
10
+ * fall back to "no App Check token" exactly as before this existed.
10
11
  *
11
- * Signs a short-lived custom JWT with the service account's private key
12
- * (`jose`, Edge/WebCrypto-compatible — no `firebase-admin`), then exchanges
13
- * it for an App Check token via `exchangeCustomToken`, authenticated with
14
- * the project's Web API key (`?key=`) — `exchangeCustomToken` otherwise
15
- * rejects the call outright as an unregistered/unidentified caller
16
- * (403 `PERMISSION_DENIED`), before the custom token itself is even
17
- * evaluated. Not cached beyond the caller's own request-scoped `cache()`
18
- * wrapper — a fresh mint costs one signing operation plus one network
19
- * round-trip, acceptable per-request but not worth doing more than once per
20
- * request.
12
+ * Signs a short-lived custom JWT, then exchanges it for an App Check token
13
+ * via `exchangeCustomToken`, authenticated with the project's Web API key
14
+ * (`?key=`) — `exchangeCustomToken` otherwise rejects the call outright as
15
+ * an unregistered/unidentified caller (403 `PERMISSION_DENIED`), before the
16
+ * custom token itself is even evaluated. Not cached beyond the caller's own
17
+ * request-scoped `cache()` wrapper — a fresh mint costs one signing
18
+ * operation plus one network round-trip, acceptable per-request but not
19
+ * worth doing more than once per request.
20
+ *
21
+ * The custom token is signed one of two ways, `privateKey` taking priority
22
+ * when both are set:
23
+ * - `privateKey` set: signed locally (`jose`, Edge/WebCrypto-compatible —
24
+ * no `firebase-admin`).
25
+ * - OAuth triple set instead: signed remotely via
26
+ * `sign_custom_token_remote.ts` (IAM Credentials `signJwt`) — the way to
27
+ * mint tokens when a GCP org policy blocks creating the service-account
28
+ * key `privateKey` would otherwise require.
21
29
  */
22
30
  export default function mintServerAppCheckToken(projectId: string, apiKey: string, appCheck: FirebaseAppCheckConfig | undefined): Promise<string | undefined>;