@prisma-next/extension-supabase 0.13.0 → 0.14.0-dev.10

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.
@@ -37,7 +37,7 @@
37
37
  "id": {
38
38
  "nullable": false,
39
39
  "type": {
40
- "codecId": "pg/text@1",
40
+ "codecId": "pg/uuid@1",
41
41
  "kind": "scalar"
42
42
  }
43
43
  },
@@ -58,7 +58,7 @@
58
58
  "user_id": {
59
59
  "nullable": false,
60
60
  "type": {
61
- "codecId": "pg/text@1",
61
+ "codecId": "pg/uuid@1",
62
62
  "kind": "scalar"
63
63
  }
64
64
  }
@@ -105,7 +105,7 @@
105
105
  "id": {
106
106
  "nullable": false,
107
107
  "type": {
108
- "codecId": "pg/text@1",
108
+ "codecId": "pg/uuid@1",
109
109
  "kind": "scalar"
110
110
  }
111
111
  },
@@ -211,7 +211,7 @@
211
211
  "id": {
212
212
  "nullable": false,
213
213
  "type": {
214
- "codecId": "pg/text@1",
214
+ "codecId": "pg/uuid@1",
215
215
  "kind": "scalar"
216
216
  }
217
217
  },
@@ -259,13 +259,6 @@
259
259
  },
260
260
  "storage": {
261
261
  "namespaces": {
262
- "__unbound__": {
263
- "entries": {
264
- "table": {}
265
- },
266
- "id": "__unbound__",
267
- "kind": "postgres-unbound-schema"
268
- },
269
262
  "auth": {
270
263
  "entries": {
271
264
  "table": {
@@ -278,7 +271,7 @@
278
271
  "typeRef": "Timestamptz"
279
272
  },
280
273
  "id": {
281
- "codecId": "pg/text@1",
274
+ "codecId": "pg/uuid@1",
282
275
  "nativeType": "uuid",
283
276
  "nullable": false,
284
277
  "typeRef": "Uuid"
@@ -295,7 +288,7 @@
295
288
  "typeRef": "Timestamptz"
296
289
  },
297
290
  "user_id": {
298
- "codecId": "pg/text@1",
291
+ "codecId": "pg/uuid@1",
299
292
  "nativeType": "uuid",
300
293
  "nullable": false,
301
294
  "typeRef": "Uuid"
@@ -324,7 +317,7 @@
324
317
  "nullable": false
325
318
  },
326
319
  "id": {
327
- "codecId": "pg/text@1",
320
+ "codecId": "pg/uuid@1",
328
321
  "nativeType": "uuid",
329
322
  "nullable": false,
330
323
  "typeRef": "Uuid"
@@ -408,7 +401,7 @@
408
401
  "typeRef": "Timestamptz"
409
402
  },
410
403
  "id": {
411
- "codecId": "pg/text@1",
404
+ "codecId": "pg/uuid@1",
412
405
  "nativeType": "uuid",
413
406
  "nullable": false,
414
407
  "typeRef": "Uuid"
@@ -440,7 +433,7 @@
440
433
  "kind": "postgres-schema"
441
434
  }
442
435
  },
443
- "storageHash": "sha256:a92ac18f82eb73f747fa9512351caec3e42df1a1a897bce787966af84371638c",
436
+ "storageHash": "sha256:61dac13aa56bcb5b7f1c5441c2bd8fc07d88f6c870c80b4c1640fd0d180fb423",
444
437
  "types": {
445
438
  "Timestamptz": {
446
439
  "codecId": "pg/timestamptz@1",
@@ -448,7 +441,7 @@
448
441
  "nativeType": "timestamptz"
449
442
  },
450
443
  "Uuid": {
451
- "codecId": "pg/text@1",
444
+ "codecId": "pg/uuid@1",
452
445
  "kind": "codec-instance",
453
446
  "nativeType": "uuid"
454
447
  }
@@ -1,31 +1,19 @@
1
- /**
2
- * Minimal M1 runtime descriptor for the Supabase extension.
3
- *
4
- * The Supabase pack contributes no runtime codec types or query operations in
5
- * M1 — `auth.*`/`storage.*` are external tables accessed via the stock
6
- * postgres runtime, not through a custom codec or operation surface. This
7
- * descriptor exists so the postgres runtime's contract-requirements check
8
- * (which verifies every `extensionPacks` entry in the emitted `contract.json`
9
- * has a matching runtime component) passes.
10
- *
11
- * TODO(M2): Replace with the real SupabaseRuntime that adds
12
- * `asUser()`/`asAnon()` role-binding and the Supabase auth surface.
13
- */
14
- import type { SqlRuntimeExtensionDescriptor } from '@prisma-next/sql-runtime';
15
-
16
- const supabaseRuntimeDescriptor: SqlRuntimeExtensionDescriptor<'postgres'> = {
17
- kind: 'extension' as const,
18
- id: 'supabase',
19
- version: '0.12.0',
20
- familyId: 'sql' as const,
21
- targetId: 'postgres' as const,
22
- codecs: () => [],
23
- create() {
24
- return {
25
- familyId: 'sql' as const,
26
- targetId: 'postgres' as const,
27
- };
28
- },
29
- };
1
+ import { supabaseRuntimeDescriptor } from '../runtime/descriptor';
30
2
 
31
3
  export default supabaseRuntimeDescriptor;
4
+
5
+ export type {
6
+ RoleBoundDb,
7
+ SupabaseDb,
8
+ SupabaseOptions,
9
+ SupabaseOptionsWithContract,
10
+ SupabaseOptionsWithContractJson,
11
+ SupabaseTargetId,
12
+ } from '../runtime/supabase';
13
+ export { default as supabase, InvalidJwtError, SupabaseConfigError } from '../runtime/supabase';
14
+ export type {
15
+ RoleSession,
16
+ SupabaseRoleBinding,
17
+ SupabaseRuntime,
18
+ } from '../runtime/supabase-runtime';
19
+ export { SupabaseRuntimeImpl } from '../runtime/supabase-runtime';
@@ -0,0 +1,26 @@
1
+ import type { SqlRuntimeExtensionDescriptor } from '@prisma-next/sql-runtime';
2
+ import packageJson from '../../package.json' with { type: 'json' };
3
+
4
+ /**
5
+ * Tells the runtime that the Supabase pack's runtime component is available.
6
+ *
7
+ * When a contract declares the Supabase pack, the runtime checks that a
8
+ * matching descriptor has been registered. Without this, loading a Supabase
9
+ * contract errors with "pack runtime component missing". The `supabase()`
10
+ * factory registers this descriptor automatically — app code never needs to
11
+ * reference it directly.
12
+ */
13
+ export const supabaseRuntimeDescriptor: SqlRuntimeExtensionDescriptor<'postgres'> = {
14
+ kind: 'extension' as const,
15
+ id: 'supabase',
16
+ version: packageJson.version,
17
+ familyId: 'sql' as const,
18
+ targetId: 'postgres' as const,
19
+ codecs: () => [],
20
+ create() {
21
+ return {
22
+ familyId: 'sql' as const,
23
+ targetId: 'postgres' as const,
24
+ };
25
+ },
26
+ };
@@ -0,0 +1,171 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { RuntimeExecuteOptions } from '@prisma-next/framework-components/runtime';
3
+ import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
4
+ import { type PostgresRuntime, PostgresRuntimeImpl } from '@prisma-next/postgres/runtime';
5
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
6
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
7
+ import type {
8
+ PreparedStatement,
9
+ PreparedStatementImpl,
10
+ RuntimeConnection,
11
+ RuntimeTransaction,
12
+ } from '@prisma-next/sql-runtime';
13
+ import { blindCast } from '@prisma-next/utils/casts';
14
+
15
+ export interface SupabaseRuntime extends PostgresRuntime {}
16
+
17
+ export interface SupabaseRoleBinding {
18
+ // TODO(TML-2501): role names move to the Supabase extension contract (roles as first-class IR) when postgres-rls lands.
19
+ readonly role: 'anon' | 'authenticated' | 'service_role';
20
+ readonly claims?: Record<string, unknown>;
21
+ }
22
+
23
+ /**
24
+ * A connection with a Supabase role already bound via session-scoped set_config.
25
+ * Implements `RuntimeConnection` so it plugs into ORM scope machinery and `withTransaction`.
26
+ */
27
+ export interface RoleSession extends RuntimeConnection {}
28
+
29
+ export class SupabaseRuntimeImpl<
30
+ TContract extends Contract<SqlStorage> = Contract<SqlStorage>,
31
+ > extends PostgresRuntimeImpl<TContract> {
32
+ /**
33
+ * Opens a raw connection and applies role + JWT claims via session-scoped set_config.
34
+ * On bind failure, destroys the connection before rethrowing — no leaked connections.
35
+ * Not on the `SupabaseRuntime` interface; consumed by the facade, not by app code.
36
+ */
37
+ async openRoleSession(binding: SupabaseRoleBinding): Promise<RoleSession> {
38
+ const conn = await this.acquireRawConnection();
39
+
40
+ try {
41
+ await conn.query('SELECT set_config($1, $2, false)', ['role', binding.role]);
42
+ await conn.query('SELECT set_config($1, $2, false)', [
43
+ 'request.jwt.claims',
44
+ JSON.stringify(binding.claims ?? {}),
45
+ ]);
46
+ } catch (err) {
47
+ await conn.destroy(err).catch(() => undefined);
48
+ throw err;
49
+ }
50
+
51
+ const self = this;
52
+
53
+ const session: RoleSession = {
54
+ execute<Row>(
55
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
56
+ options?: RuntimeExecuteOptions,
57
+ ): AsyncIterableResult<Row> {
58
+ return self.executeAgainstQueryable<Row>(plan, conn, { ...options, scope: 'connection' });
59
+ },
60
+
61
+ executePrepared<Params, Row>(
62
+ ps: PreparedStatement<Params, Row>,
63
+ params: Params,
64
+ options?: RuntimeExecuteOptions,
65
+ ): AsyncIterableResult<Row> {
66
+ return self.executePreparedAgainstQueryable(
67
+ blindCast<
68
+ PreparedStatementImpl<Params, Row>,
69
+ 'PreparedStatement is PreparedStatementImpl; the impl class is the only concrete form'
70
+ >(ps),
71
+ blindCast<
72
+ Record<string, unknown>,
73
+ 'params are structurally Record<string, unknown> at runtime'
74
+ >(params),
75
+ conn,
76
+ { ...options, scope: 'connection' },
77
+ );
78
+ },
79
+
80
+ async transaction(): Promise<RuntimeTransaction> {
81
+ const tx = await conn.beginTransaction();
82
+ return {
83
+ async commit(): Promise<void> {
84
+ await tx.commit();
85
+ },
86
+ async rollback(): Promise<void> {
87
+ await tx.rollback();
88
+ },
89
+ execute<Row>(
90
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
91
+ options?: RuntimeExecuteOptions,
92
+ ): AsyncIterableResult<Row> {
93
+ return self.executeAgainstQueryable<Row>(plan, tx, {
94
+ ...options,
95
+ scope: 'transaction',
96
+ });
97
+ },
98
+ executePrepared<Params, Row>(
99
+ ps: PreparedStatement<Params, Row>,
100
+ params: Params,
101
+ options?: RuntimeExecuteOptions,
102
+ ): AsyncIterableResult<Row> {
103
+ return self.executePreparedAgainstQueryable(
104
+ blindCast<
105
+ PreparedStatementImpl<Params, Row>,
106
+ 'PreparedStatement is PreparedStatementImpl; the impl class is the only concrete form'
107
+ >(ps),
108
+ blindCast<
109
+ Record<string, unknown>,
110
+ 'params are structurally Record<string, unknown> at runtime'
111
+ >(params),
112
+ tx,
113
+ { ...options, scope: 'transaction' },
114
+ );
115
+ },
116
+ };
117
+ },
118
+
119
+ /**
120
+ * Resets all session-local config then releases the connection back to the pool.
121
+ * If RESET ALL fails, destroys the connection instead — pool-poisoning guarantee.
122
+ */
123
+ async release(): Promise<void> {
124
+ try {
125
+ await conn.query('RESET ALL');
126
+ await conn.release();
127
+ } catch (resetError) {
128
+ await conn.destroy(resetError).catch(() => undefined);
129
+ }
130
+ },
131
+
132
+ async destroy(reason?: unknown): Promise<void> {
133
+ await conn.destroy(reason);
134
+ },
135
+ };
136
+
137
+ return session;
138
+ }
139
+
140
+ /**
141
+ * Opens a role session, executes the plan, then releases after the stream drains.
142
+ * On mid-stream error, destroys the session instead of releasing.
143
+ */
144
+ executeWithRole<Row>(
145
+ plan: SqlExecutionPlan<Row> | SqlQueryPlan<Row>,
146
+ binding: SupabaseRoleBinding,
147
+ options?: RuntimeExecuteOptions,
148
+ ): AsyncIterableResult<Row> {
149
+ const self = this;
150
+
151
+ const generator = async function* (): AsyncGenerator<Row, void, unknown> {
152
+ const session = await self.openRoleSession(binding);
153
+ let errored = false;
154
+ try {
155
+ for await (const row of session.execute(plan, options)) {
156
+ yield row;
157
+ }
158
+ } catch (err) {
159
+ errored = true;
160
+ await session.destroy(err).catch(() => undefined);
161
+ throw err;
162
+ } finally {
163
+ if (!errored) {
164
+ await session.release();
165
+ }
166
+ }
167
+ };
168
+
169
+ return new AsyncIterableResult(generator());
170
+ }
171
+ }
@@ -0,0 +1,323 @@
1
+ import postgresAdapter from '@prisma-next/adapter-postgres/runtime';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import postgresDriver from '@prisma-next/driver-postgres/runtime';
4
+ import { instantiateExecutionStack } from '@prisma-next/framework-components/execution';
5
+ import type {
6
+ AsyncIterableResult,
7
+ RuntimeExecuteOptions,
8
+ } from '@prisma-next/framework-components/runtime';
9
+ import { sql } from '@prisma-next/sql-builder/runtime';
10
+ import type { Db } from '@prisma-next/sql-builder/types';
11
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
12
+ import { orm } from '@prisma-next/sql-orm-client';
13
+ import type { RawSqlTag } from '@prisma-next/sql-relational-core/expression';
14
+ import { createRawSql } from '@prisma-next/sql-relational-core/expression';
15
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
16
+ import type {
17
+ ExecutionContext,
18
+ SqlExecutionStackWithDriver,
19
+ SqlMiddleware,
20
+ SqlRuntimeExtensionDescriptor,
21
+ TransactionContext,
22
+ VerifyMarkerOption,
23
+ } from '@prisma-next/sql-runtime';
24
+ import {
25
+ createExecutionContext,
26
+ createSqlExecutionStack,
27
+ withTransaction,
28
+ } from '@prisma-next/sql-runtime';
29
+ import postgresTarget, { PostgresContractSerializer } from '@prisma-next/target-postgres/runtime';
30
+ import { blindCast } from '@prisma-next/utils/casts';
31
+ import { ifDefined } from '@prisma-next/utils/defined';
32
+ import { createRemoteJWKSet, type JWTVerifyResult, jwtVerify } from 'jose';
33
+ import type { Client } from 'pg';
34
+ import { Pool } from 'pg';
35
+ import { supabaseRuntimeDescriptor } from './descriptor';
36
+ import type { SupabaseRoleBinding, SupabaseRuntime } from './supabase-runtime';
37
+ import { SupabaseRuntimeImpl } from './supabase-runtime';
38
+
39
+ export type SupabaseTargetId = 'postgres';
40
+
41
+ type OrmClient<TContract extends Contract<SqlStorage>> = ReturnType<typeof orm<TContract>>;
42
+
43
+ export class SupabaseConfigError extends Error {
44
+ override readonly name = 'SupabaseConfigError';
45
+ constructor(message: string) {
46
+ super(message);
47
+ }
48
+ }
49
+
50
+ export class InvalidJwtError extends Error {
51
+ override readonly name = 'InvalidJwtError';
52
+ readonly reason: string;
53
+ constructor(reason: string) {
54
+ super(`Invalid JWT: ${reason}`);
55
+ this.reason = reason;
56
+ }
57
+ }
58
+
59
+ type KeyMaterial =
60
+ | { readonly kind: 'secret'; readonly key: Uint8Array }
61
+ | { readonly kind: 'jwks'; readonly keyset: ReturnType<typeof createRemoteJWKSet> };
62
+
63
+ export interface RoleBoundDb<TContract extends Contract<SqlStorage>> {
64
+ readonly sql: Db<TContract>;
65
+ readonly orm: OrmClient<TContract>;
66
+ readonly raw: RawSqlTag;
67
+ execute<Row>(
68
+ plan: (SqlExecutionPlan<Row> | SqlQueryPlan<Row>) & { readonly _row?: Row },
69
+ options?: RuntimeExecuteOptions,
70
+ ): AsyncIterableResult<Row>;
71
+ transaction<R>(fn: (tx: TransactionContext) => PromiseLike<R>): Promise<R>;
72
+ }
73
+
74
+ export interface SupabaseDb<TContract extends Contract<SqlStorage>> {
75
+ readonly context: ExecutionContext<TContract>;
76
+ readonly stack: SqlExecutionStackWithDriver<SupabaseTargetId>;
77
+ asUser(jwt: string): Promise<RoleBoundDb<TContract>>;
78
+ asAnon(): RoleBoundDb<TContract>;
79
+ asServiceRole(): RoleBoundDb<TContract>;
80
+ close(): Promise<void>;
81
+ [Symbol.asyncDispose](): Promise<void>;
82
+ }
83
+
84
+ export interface SupabaseOptionsBase {
85
+ readonly extensions?: readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[];
86
+ readonly middleware?: readonly SqlMiddleware[];
87
+ readonly verifyMarker?: VerifyMarkerOption;
88
+ readonly poolOptions?: {
89
+ readonly connectionTimeoutMillis?: number;
90
+ readonly idleTimeoutMillis?: number;
91
+ };
92
+ }
93
+
94
+ export interface SupabaseBindingOptions {
95
+ readonly url?: string;
96
+ readonly pg?: Pool | Client;
97
+ }
98
+
99
+ type JwtSecretOption = {
100
+ readonly jwtSecret: string;
101
+ readonly jwksUrl?: never;
102
+ };
103
+
104
+ type JwksUrlOption = {
105
+ readonly jwksUrl: string;
106
+ readonly jwtSecret?: never;
107
+ };
108
+
109
+ export type SupabaseOptionsWithContract<TContract extends Contract<SqlStorage>> =
110
+ SupabaseBindingOptions &
111
+ SupabaseOptionsBase &
112
+ (JwtSecretOption | JwksUrlOption) & {
113
+ readonly contract: TContract;
114
+ readonly contractJson?: never;
115
+ };
116
+
117
+ export type SupabaseOptionsWithContractJson<TContract extends Contract<SqlStorage>> =
118
+ SupabaseBindingOptions &
119
+ SupabaseOptionsBase &
120
+ (JwtSecretOption | JwksUrlOption) & {
121
+ readonly contractJson: unknown;
122
+ readonly contract?: never;
123
+ readonly _contract?: TContract;
124
+ };
125
+
126
+ export type SupabaseOptions<TContract extends Contract<SqlStorage>> =
127
+ | SupabaseOptionsWithContract<TContract>
128
+ | SupabaseOptionsWithContractJson<TContract>;
129
+
130
+ function hasContractJson<TContract extends Contract<SqlStorage>>(
131
+ options: SupabaseOptions<TContract>,
132
+ ): options is SupabaseOptionsWithContractJson<TContract> {
133
+ return 'contractJson' in options;
134
+ }
135
+
136
+ const contractSerializer = new PostgresContractSerializer();
137
+
138
+ function resolveContract<TContract extends Contract<SqlStorage>>(
139
+ options: SupabaseOptions<TContract>,
140
+ ): TContract {
141
+ const contractInput = hasContractJson(options) ? options.contractJson : options.contract;
142
+ return blindCast<
143
+ TContract,
144
+ 'contractSerializer.deserializeContract returns a validated TContract'
145
+ >(contractSerializer.deserializeContract(contractInput));
146
+ }
147
+
148
+ function resolveKeyMaterial<TContract extends Contract<SqlStorage>>(
149
+ options: SupabaseOptions<TContract>,
150
+ ): KeyMaterial {
151
+ const jwtSecret = 'jwtSecret' in options ? options.jwtSecret : undefined;
152
+ const jwksUrl = 'jwksUrl' in options ? options.jwksUrl : undefined;
153
+
154
+ if (jwtSecret !== undefined && jwksUrl !== undefined) {
155
+ throw new SupabaseConfigError('Provide either jwtSecret or jwksUrl, not both');
156
+ }
157
+ if (jwtSecret === undefined && jwksUrl === undefined) {
158
+ throw new SupabaseConfigError('Either jwtSecret or jwksUrl is required');
159
+ }
160
+
161
+ if (jwtSecret !== undefined) {
162
+ return { kind: 'secret', key: new TextEncoder().encode(jwtSecret) };
163
+ }
164
+
165
+ if (jwksUrl !== undefined) {
166
+ return { kind: 'jwks', keyset: createRemoteJWKSet(new URL(jwksUrl)) };
167
+ }
168
+
169
+ throw new SupabaseConfigError('Either jwtSecret or jwksUrl is required');
170
+ }
171
+
172
+ function toPool<TContract extends Contract<SqlStorage>>(
173
+ options: SupabaseOptions<TContract>,
174
+ ): { pool: Pool; owned: boolean } | undefined {
175
+ if (options.pg instanceof Pool) {
176
+ return { pool: options.pg, owned: false };
177
+ }
178
+ if (typeof options.url === 'string') {
179
+ return {
180
+ pool: new Pool({
181
+ connectionString: options.url,
182
+ connectionTimeoutMillis: options.poolOptions?.connectionTimeoutMillis ?? 20_000,
183
+ idleTimeoutMillis: options.poolOptions?.idleTimeoutMillis ?? 30_000,
184
+ }),
185
+ owned: true,
186
+ };
187
+ }
188
+ return undefined;
189
+ }
190
+
191
+ function withSupabaseDescriptor(
192
+ extensions: readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[] | undefined,
193
+ ): readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[] {
194
+ const packs = extensions ?? [];
195
+ return packs.some((pack) => pack.id === supabaseRuntimeDescriptor.id)
196
+ ? packs
197
+ : [...packs, supabaseRuntimeDescriptor];
198
+ }
199
+
200
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
201
+ options: SupabaseOptionsWithContract<TContract>,
202
+ ): Promise<SupabaseDb<TContract>>;
203
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
204
+ options: SupabaseOptionsWithContractJson<TContract>,
205
+ ): Promise<SupabaseDb<TContract>>;
206
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
207
+ options: SupabaseOptions<TContract>,
208
+ ): Promise<SupabaseDb<TContract>> {
209
+ const keyMaterial = resolveKeyMaterial(options);
210
+ const contract = resolveContract(options);
211
+
212
+ const stack = createSqlExecutionStack({
213
+ target: postgresTarget,
214
+ adapter: postgresAdapter,
215
+ driver: postgresDriver,
216
+ extensionPacks: withSupabaseDescriptor(options.extensions),
217
+ });
218
+
219
+ const context = createExecutionContext({ contract, stack });
220
+ const rawCodecInferer = stack.adapter.rawCodecInferer;
221
+ const rawSqlTag: RawSqlTag = createRawSql(rawCodecInferer);
222
+
223
+ const poolEntry = toPool(options);
224
+ let closed = false;
225
+
226
+ const stackInstance = instantiateExecutionStack(stack);
227
+ const driverDescriptor = stack.driver;
228
+ if (!driverDescriptor) {
229
+ throw new Error('Driver descriptor missing from execution stack');
230
+ }
231
+ const driver = driverDescriptor.create({ cursor: { disabled: true } });
232
+
233
+ if (poolEntry) {
234
+ await driver.connect({ kind: 'pgPool', pool: poolEntry.pool });
235
+ }
236
+
237
+ const runtime: SupabaseRuntime & SupabaseRuntimeImpl<TContract> = new SupabaseRuntimeImpl({
238
+ context,
239
+ adapter: stackInstance.adapter,
240
+ driver,
241
+ ...ifDefined('verifyMarker', options.verifyMarker),
242
+ ...ifDefined('middleware', options.middleware),
243
+ });
244
+
245
+ async function verifyJwt(jwt: string): Promise<JWTVerifyResult> {
246
+ try {
247
+ if (keyMaterial.kind === 'secret') {
248
+ return await jwtVerify(jwt, keyMaterial.key);
249
+ }
250
+ return await jwtVerify(jwt, keyMaterial.keyset);
251
+ } catch (err) {
252
+ const reason = err instanceof Error ? err.message : String(err);
253
+ throw new InvalidJwtError(reason);
254
+ }
255
+ }
256
+
257
+ function buildRoleBoundDb(binding: SupabaseRoleBinding): RoleBoundDb<TContract> {
258
+ const roleSql: Db<TContract> = sql<TContract>({ context, rawCodecInferer });
259
+ const roleOrm: OrmClient<TContract> = orm({
260
+ runtime: {
261
+ execute(plan) {
262
+ return runtime.executeWithRole(plan, binding);
263
+ },
264
+ // connection() returns a role session; this is the enforcement path for ORM scope
265
+ // operations (mutations, includes) — every statement runs role-bound.
266
+ connection: () => runtime.openRoleSession(binding),
267
+ },
268
+ context,
269
+ });
270
+
271
+ return {
272
+ sql: roleSql,
273
+ orm: roleOrm,
274
+ raw: rawSqlTag,
275
+ execute<Row>(
276
+ plan: (SqlExecutionPlan<Row> | SqlQueryPlan<Row>) & { readonly _row?: Row },
277
+ execOptions?: RuntimeExecuteOptions,
278
+ ): AsyncIterableResult<Row> {
279
+ return runtime.executeWithRole<Row>(plan, binding, execOptions);
280
+ },
281
+ transaction<R>(fn: (tx: TransactionContext) => PromiseLike<R>): Promise<R> {
282
+ return withTransaction({ connection: () => runtime.openRoleSession(binding) }, fn);
283
+ },
284
+ };
285
+ }
286
+
287
+ async function closeDb(): Promise<void> {
288
+ if (closed) return;
289
+ closed = true;
290
+ await runtime.close();
291
+ if (poolEntry?.owned) {
292
+ await poolEntry.pool.end().catch(() => undefined);
293
+ }
294
+ }
295
+
296
+ return {
297
+ context,
298
+ stack,
299
+
300
+ async asUser(jwt: string): Promise<RoleBoundDb<TContract>> {
301
+ const { payload } = await verifyJwt(jwt);
302
+ const rawRole = payload['role'];
303
+ const roleStr = typeof rawRole === 'string' ? rawRole : 'authenticated';
304
+ const role: SupabaseRoleBinding['role'] =
305
+ roleStr === 'anon' || roleStr === 'authenticated' || roleStr === 'service_role'
306
+ ? roleStr
307
+ : 'authenticated';
308
+ const binding: SupabaseRoleBinding = { role, claims: payload };
309
+ return buildRoleBoundDb(binding);
310
+ },
311
+
312
+ asAnon(): RoleBoundDb<TContract> {
313
+ return buildRoleBoundDb({ role: 'anon', claims: {} });
314
+ },
315
+
316
+ asServiceRole(): RoleBoundDb<TContract> {
317
+ return buildRoleBoundDb({ role: 'service_role', claims: {} });
318
+ },
319
+
320
+ close: closeDb,
321
+ [Symbol.asyncDispose]: closeDb,
322
+ };
323
+ }