tinbase 0.5.0 → 0.6.1

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 (70) hide show
  1. package/README.md +29 -26
  2. package/dist/admin/api.d.ts +3 -1
  3. package/dist/admin/api.js +9 -1
  4. package/dist/admin/api.js.map +1 -1
  5. package/dist/admin/ui.d.ts +1 -1
  6. package/dist/admin/ui.js +7 -7
  7. package/dist/auth/handler.d.ts +6 -1
  8. package/dist/auth/handler.js +148 -6
  9. package/dist/auth/handler.js.map +1 -1
  10. package/dist/auth/inbox.d.ts +31 -0
  11. package/dist/auth/inbox.js +112 -0
  12. package/dist/auth/inbox.js.map +1 -0
  13. package/dist/auth/qr.d.ts +8 -0
  14. package/dist/auth/qr.js +376 -0
  15. package/dist/auth/qr.js.map +1 -0
  16. package/dist/auth/totp.d.ts +19 -0
  17. package/dist/auth/totp.js +101 -0
  18. package/dist/auth/totp.js.map +1 -0
  19. package/dist/cli.js +86 -15
  20. package/dist/cli.js.map +1 -1
  21. package/dist/db/bootstrap.d.ts +8 -1
  22. package/dist/db/bootstrap.js +197 -0
  23. package/dist/db/bootstrap.js.map +1 -1
  24. package/dist/db/database.d.ts +21 -0
  25. package/dist/db/database.js +46 -6
  26. package/dist/db/database.js.map +1 -1
  27. package/dist/db/engine.d.ts +2 -0
  28. package/dist/db/engine.js.map +1 -1
  29. package/dist/db/inspect.d.ts +13 -0
  30. package/dist/db/inspect.js +22 -0
  31. package/dist/db/inspect.js.map +1 -0
  32. package/dist/db/pgmem-engine.d.ts +22 -0
  33. package/dist/db/pgmem-engine.js +135 -0
  34. package/dist/db/pgmem-engine.js.map +1 -0
  35. package/dist/functions/handler.d.ts +3 -1
  36. package/dist/functions/handler.js.map +1 -1
  37. package/dist/index.d.ts +11 -0
  38. package/dist/index.js +49 -10
  39. package/dist/index.js.map +1 -1
  40. package/dist/jwt.d.ts +7 -0
  41. package/dist/jwt.js.map +1 -1
  42. package/dist/log-buffer.d.ts +21 -0
  43. package/dist/log-buffer.js +20 -0
  44. package/dist/log-buffer.js.map +1 -0
  45. package/dist/node/bundle-function.d.ts +7 -0
  46. package/dist/node/bundle-function.js +90 -0
  47. package/dist/node/bundle-function.js.map +1 -0
  48. package/dist/node/db-diff.d.ts +21 -0
  49. package/dist/node/db-diff.js +42 -0
  50. package/dist/node/db-diff.js.map +1 -1
  51. package/dist/node/index.d.ts +4 -3
  52. package/dist/node/index.js +4 -3
  53. package/dist/node/index.js.map +1 -1
  54. package/dist/node/load-functions.d.ts +6 -0
  55. package/dist/node/load-functions.js +49 -5
  56. package/dist/node/load-functions.js.map +1 -1
  57. package/dist/node/server.d.ts +6 -0
  58. package/dist/node/server.js +27 -1
  59. package/dist/node/server.js.map +1 -1
  60. package/dist/realtime/engine.d.ts +10 -0
  61. package/dist/realtime/engine.js +90 -1
  62. package/dist/realtime/engine.js.map +1 -1
  63. package/dist/rest/build.d.ts +5 -1
  64. package/dist/rest/build.js +17 -8
  65. package/dist/rest/build.js.map +1 -1
  66. package/dist/rest/handler.js +22 -9
  67. package/dist/rest/handler.js.map +1 -1
  68. package/dist/types.d.ts +2 -0
  69. package/dist/types.js.map +1 -1
  70. package/package.json +5 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Bundle an edge function with esbuild so it runs under Node like it would on
3
+ * Supabase's Deno edge runtime. This resolves what a plain `import()` can't:
4
+ * - TypeScript and multi-file functions (relative imports)
5
+ * - `npm:` / `jsr:` specifiers → rewritten to esm.sh and fetched
6
+ * - `https://` URL imports → fetched (and their transitive deps)
7
+ *
8
+ * Remote modules are fetched at bundle time (first run) and disk-cached, the
9
+ * same trade Deno makes. esbuild is an optional dependency; when it's missing
10
+ * the loader falls back to a plain import (Web-API/Deno.serve functions still
11
+ * work, just not remote/TS imports).
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
15
+ import { existsSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ /** Map a Deno-style specifier to a fetchable URL. Pure — unit tested. */
19
+ export function rewriteRemoteSpecifier(spec) {
20
+ if (spec.startsWith('npm:'))
21
+ return `https://esm.sh/${spec.slice(4)}`;
22
+ if (spec.startsWith('jsr:'))
23
+ return `https://esm.sh/jsr/${spec.slice(4)}`;
24
+ return spec; // already an http(s) URL
25
+ }
26
+ const HTTP_CACHE = join(tmpdir(), 'tinbase-fn-http');
27
+ async function fetchModule(url) {
28
+ const key = createHash('sha256').update(url).digest('hex');
29
+ const cached = join(HTTP_CACHE, key);
30
+ if (existsSync(cached))
31
+ return readFile(cached, 'utf8');
32
+ const res = await fetch(url, { redirect: 'follow' });
33
+ if (!res.ok)
34
+ throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
35
+ const body = await res.text();
36
+ await mkdir(HTTP_CACHE, { recursive: true });
37
+ await writeFile(cached, body);
38
+ return body;
39
+ }
40
+ /** esbuild plugin: resolve npm:/jsr:/https imports by fetching from esm.sh. */
41
+ function remotePlugin() {
42
+ return {
43
+ name: 'tinbase-remote',
44
+ setup(build) {
45
+ build.onResolve({ filter: /^(npm:|jsr:|https?:\/\/)/ }, (args) => ({
46
+ path: rewriteRemoteSpecifier(args.path),
47
+ namespace: 'http-url',
48
+ }));
49
+ // imports *inside* a fetched module resolve relative to its URL
50
+ build.onResolve({ filter: /.*/, namespace: 'http-url' }, (args) => ({
51
+ path: new URL(args.path, args.importer).href,
52
+ namespace: 'http-url',
53
+ }));
54
+ build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => ({
55
+ contents: await fetchModule(args.path),
56
+ loader: 'js',
57
+ }));
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * Bundle `entryPath` to a single ESM file and return its path (written under
63
+ * the OS temp dir). Throws if esbuild is not installed.
64
+ */
65
+ export async function bundleFunction(entryPath, name) {
66
+ let esbuild;
67
+ try {
68
+ esbuild = await import('esbuild');
69
+ }
70
+ catch {
71
+ throw new Error('esbuild-not-available');
72
+ }
73
+ const outDir = join(tmpdir(), 'tinbase-fn-bundle');
74
+ await mkdir(outDir, { recursive: true });
75
+ const result = await esbuild.build({
76
+ entryPoints: [entryPath],
77
+ bundle: true,
78
+ format: 'esm',
79
+ platform: 'node',
80
+ target: 'node18',
81
+ write: false,
82
+ logLevel: 'silent',
83
+ plugins: [remotePlugin()],
84
+ });
85
+ const code = result.outputFiles[0].text;
86
+ const out = join(outDir, `${name}-${createHash('sha256').update(entryPath).digest('hex').slice(0, 8)}.mjs`);
87
+ await writeFile(out, code);
88
+ return out;
89
+ }
90
+ //# sourceMappingURL=bundle-function.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-function.js","sourceRoot":"","sources":["../../src/node/bundle-function.ts"],"names":[],"mappings":"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"}
@@ -11,3 +11,24 @@ export interface DbDiffOptions {
11
11
  }
12
12
  export declare function computeDbDiff(opts: DbDiffOptions): Promise<string[]>;
13
13
  export declare function shadowNativeDataDir(): string;
14
+ export interface DbPullOptions extends DbDiffOptions {
15
+ /** directory to write the migration into (usually supabase/migrations); omit to skip writing */
16
+ migrationsDir?: string;
17
+ /** migration name suffix (default 'remote_schema') */
18
+ name?: string;
19
+ /** timestamp version prefix; pass for determinism (default: now as YYYYMMDDHHMMSS) */
20
+ stamp?: string;
21
+ }
22
+ export interface DbPullResult {
23
+ ddl: string[];
24
+ version: string | null;
25
+ path: string | null;
26
+ }
27
+ /**
28
+ * `tinbase db pull` core: like `db diff`, but writes the delta as a migration
29
+ * AND records it as already-applied on the live database — the schema is
30
+ * already there, so a subsequent `tinbase start` must not re-run it. This is
31
+ * how you bring an out-of-migration (or externally-created) schema under
32
+ * version control.
33
+ */
34
+ export declare function pullSchema(opts: DbPullOptions): Promise<DbPullResult>;
@@ -5,6 +5,7 @@
5
5
  * new migration.
6
6
  */
7
7
  import { mkdtempSync } from 'node:fs';
8
+ import { mkdir, writeFile } from 'node:fs/promises';
8
9
  import { tmpdir } from 'node:os';
9
10
  import { join } from 'node:path';
10
11
  import { createBackend } from '../index.js';
@@ -37,4 +38,45 @@ export async function computeDbDiff(opts) {
37
38
  export function shadowNativeDataDir() {
38
39
  return join(mkdtempSync(join(tmpdir(), 'tinbase-shadow-')), 'pg');
39
40
  }
41
+ /**
42
+ * `tinbase db pull` core: like `db diff`, but writes the delta as a migration
43
+ * AND records it as already-applied on the live database — the schema is
44
+ * already there, so a subsequent `tinbase start` must not re-run it. This is
45
+ * how you bring an out-of-migration (or externally-created) schema under
46
+ * version control.
47
+ */
48
+ export async function pullSchema(opts) {
49
+ const schema = opts.schema ?? 'public';
50
+ const shadow = await createBackend({
51
+ engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
52
+ migrations: opts.migrations,
53
+ });
54
+ const live = await createBackend({
55
+ engine: opts.liveEngine,
56
+ dataDir: opts.liveEngine ? undefined : opts.liveDataDir,
57
+ migrations: opts.migrations,
58
+ });
59
+ try {
60
+ const ddl = diffSchemas(await snapshotSchema(shadow.db, schema), await snapshotSchema(live.db, schema), schema);
61
+ if (ddl.length === 0)
62
+ return { ddl, version: null, path: null };
63
+ const stamp = opts.stamp ?? new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
64
+ const name = opts.name ?? 'remote_schema';
65
+ const body = ddl.join('\n\n') + '\n';
66
+ let path = null;
67
+ if (opts.migrationsDir) {
68
+ await mkdir(opts.migrationsDir, { recursive: true });
69
+ path = join(opts.migrationsDir, `${stamp}_${name}.sql`);
70
+ await writeFile(path, body);
71
+ }
72
+ // record as already applied on the live DB so `start` won't re-run it
73
+ await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
74
+ values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
75
+ return { ddl, version: stamp, path };
76
+ }
77
+ finally {
78
+ await shadow.close();
79
+ await live.close();
80
+ }
81
+ }
40
82
  //# sourceMappingURL=db-diff.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"db-diff.js","sourceRoot":"","sources":["../../src/node/db-diff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,aAAa,EAAuB,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAclE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAmB;IACrD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAA;IAEtC,kCAAkC;IAClC,MAAM,MAAM,GAAmB,MAAM,aAAa,CAAC;QACjD,MAAM,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,SAAS;QACzE,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,oCAAoC;KACrC,CAAC,CAAA;IACF,iFAAiF;IACjF,8EAA8E;IAC9E,MAAM,IAAI,GAAmB,MAAM,aAAa,CAAC;QAC/C,MAAM,EAAE,IAAI,CAAC,UAAU;QACvB,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW;QACvD,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC,CAAA;IAEF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QAC1D,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QACtD,OAAO,WAAW,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;IAClD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;QACpB,MAAM,IAAI,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;AACnE,CAAC"}
1
+ {"version":3,"file":"db-diff.js","sourceRoot":"","sources":["../../src/node/db-diff.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,11 @@
1
1
  /** Node-only entrypoints (HTTP server, fs storage, project loading). */
2
- export { serve, type ServeOptions, type RunningServer } from './server.js';
2
+ export { serve, findAvailablePort, type ServeOptions, type RunningServer } from './server.js';
3
3
  export { FsStorageDriver } from './fs-driver.js';
4
4
  export { loadSupabaseProject, type SupabaseProject } from './project.js';
5
5
  export { acceptWebSocket, type WsConnection } from './ws.js';
6
6
  export { createNativeEngine, ensurePostgres, type NativeEngineOptions } from './native/engine.js';
7
7
  export { PgWireClient, PgWireError } from './native/wire.js';
8
- export { loadFunctions } from './load-functions.js';
8
+ export { loadFunctions, loadFunctionEnv } from './load-functions.js';
9
+ export { bundleFunction, rewriteRemoteSpecifier } from './bundle-function.js';
9
10
  export { loadOAuthProviders } from './load-oauth.js';
10
- export { computeDbDiff, shadowNativeDataDir, type DbDiffOptions } from './db-diff.js';
11
+ export { computeDbDiff, pullSchema, shadowNativeDataDir, type DbDiffOptions, type DbPullOptions, type DbPullResult } from './db-diff.js';
@@ -1,11 +1,12 @@
1
1
  /** Node-only entrypoints (HTTP server, fs storage, project loading). */
2
- export { serve } from './server.js';
2
+ export { serve, findAvailablePort } from './server.js';
3
3
  export { FsStorageDriver } from './fs-driver.js';
4
4
  export { loadSupabaseProject } from './project.js';
5
5
  export { acceptWebSocket } from './ws.js';
6
6
  export { createNativeEngine, ensurePostgres } from './native/engine.js';
7
7
  export { PgWireClient, PgWireError } from './native/wire.js';
8
- export { loadFunctions } from './load-functions.js';
8
+ export { loadFunctions, loadFunctionEnv } from './load-functions.js';
9
+ export { bundleFunction, rewriteRemoteSpecifier } from './bundle-function.js';
9
10
  export { loadOAuthProviders } from './load-oauth.js';
10
- export { computeDbDiff, shadowNativeDataDir } from './db-diff.js';
11
+ export { computeDbDiff, pullSchema, shadowNativeDataDir } from './db-diff.js';
11
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,OAAO,EAAE,KAAK,EAAyC,MAAM,aAAa,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,mBAAmB,EAAwB,MAAM,cAAc,CAAA;AACxE,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAA4B,MAAM,oBAAoB,CAAA;AACjG,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAsB,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAyC,MAAM,aAAa,CAAA;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,mBAAmB,EAAwB,MAAM,cAAc,CAAA;AACxE,OAAO,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAA4B,MAAM,oBAAoB,CAAA;AACjG,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,mBAAmB,EAA6D,MAAM,cAAc,CAAA"}
@@ -1,2 +1,8 @@
1
1
  import type { EdgeFunction } from '../functions/handler.js';
2
+ /**
3
+ * Load edge-function secrets from supabase/functions/.env (KEY=VALUE lines,
4
+ * `#` comments). These are exposed to functions via Deno.env and ctx.env —
5
+ * the local equivalent of `supabase functions serve --env-file`.
6
+ */
7
+ export declare function loadFunctionEnv(projectDir: string): Promise<Record<string, string>>;
2
8
  export declare function loadFunctions(projectDir: string): Promise<Map<string, EdgeFunction>>;
@@ -1,8 +1,40 @@
1
1
  /** Loads edge functions from supabase/functions/<name>/index.{ts,js,mjs} (Node only). */
2
- import { readdir, stat } from 'node:fs/promises';
2
+ import { readdir, readFile, stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { installDenoShim, resetCapturedHandler, takeCapturedHandler } from '../functions/deno-shim.js';
6
+ import { bundleFunction } from './bundle-function.js';
7
+ /**
8
+ * Load edge-function secrets from supabase/functions/.env (KEY=VALUE lines,
9
+ * `#` comments). These are exposed to functions via Deno.env and ctx.env —
10
+ * the local equivalent of `supabase functions serve --env-file`.
11
+ */
12
+ export async function loadFunctionEnv(projectDir) {
13
+ let text;
14
+ try {
15
+ text = await readFile(join(projectDir, 'supabase', 'functions', '.env'), 'utf8');
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ const env = {};
21
+ for (const raw of text.split('\n')) {
22
+ const line = raw.trim();
23
+ if (!line || line.startsWith('#'))
24
+ continue;
25
+ const eq = line.indexOf('=');
26
+ if (eq === -1)
27
+ continue;
28
+ const key = line.slice(0, eq).trim();
29
+ let val = line.slice(eq + 1).trim();
30
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
31
+ val = val.slice(1, -1);
32
+ }
33
+ if (key)
34
+ env[key] = val;
35
+ }
36
+ return env;
37
+ }
6
38
  export async function loadFunctions(projectDir) {
7
39
  const functions = new Map();
8
40
  const root = join(projectDir, 'supabase', 'functions');
@@ -21,7 +53,7 @@ export async function loadFunctions(projectDir) {
21
53
  const dir = join(root, name);
22
54
  if (!(await stat(dir)).isDirectory())
23
55
  continue;
24
- for (const file of ['index.ts', 'index.js', 'index.mjs']) {
56
+ for (const file of ['index.ts', 'index.tsx', 'index.js', 'index.mjs']) {
25
57
  const path = join(dir, file);
26
58
  try {
27
59
  await stat(path);
@@ -30,8 +62,20 @@ export async function loadFunctions(projectDir) {
30
62
  continue;
31
63
  }
32
64
  try {
65
+ // Bundle with esbuild so TS, relative imports, and npm:/jsr:/URL
66
+ // specifiers resolve. If esbuild isn't installed, import the file
67
+ // directly (Web-API / Deno.serve functions still work).
68
+ let importUrl;
69
+ try {
70
+ importUrl = pathToFileURL(await bundleFunction(path, name)).href;
71
+ }
72
+ catch (e) {
73
+ if (e.message !== 'esbuild-not-available')
74
+ throw e;
75
+ importUrl = pathToFileURL(path).href;
76
+ }
33
77
  resetCapturedHandler();
34
- const mod = (await import(pathToFileURL(path).href));
78
+ const mod = (await import(importUrl));
35
79
  // prefer an explicit default export; otherwise use the handler a
36
80
  // Deno.serve() call captured during import
37
81
  const denoHandler = takeCapturedHandler();
@@ -45,8 +89,8 @@ export async function loadFunctions(projectDir) {
45
89
  }
46
90
  catch (e) {
47
91
  const msg = e instanceof Error ? e.message : String(e);
48
- if (file === 'index.ts' && /Unknown file extension|Cannot find module/.test(msg)) {
49
- console.warn(` warning: function "${name}" is TypeScript — run tinbase with tsx or bun, or compile it to .js`);
92
+ if (/Unknown file extension|Cannot find module/.test(msg)) {
93
+ console.warn(` warning: function "${name}" needs bundling — install esbuild, or run tinbase with tsx/bun`);
50
94
  }
51
95
  else {
52
96
  console.warn(` warning: failed to load function "${name}": ${msg}`);
@@ -1 +1 @@
1
- {"version":3,"file":"load-functions.js","sourceRoot":"","sources":["../../src/node/load-functions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"load-functions.js","sourceRoot":"","sources":["../../src/node/load-functions.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,12 @@
1
1
  /** Node HTTP wrapper: serves the backend's fetch handler and upgrades /realtime/v1 WebSockets. */
2
2
  import { type Server } from 'node:http';
3
3
  import type { TinbaseBackend } from '../index.js';
4
+ /**
5
+ * First free port at or after `startPort` (scanning up to `maxTries`), or null
6
+ * if none. Used so `tinbase start` skips a port that's already in use (e.g. a
7
+ * tinbase instance already running) instead of crashing with EADDRINUSE.
8
+ */
9
+ export declare function findAvailablePort(startPort: number, host?: string, maxTries?: number): Promise<number | null>;
4
10
  export interface ServeOptions {
5
11
  port?: number;
6
12
  host?: string;
@@ -1,6 +1,28 @@
1
1
  /** Node HTTP wrapper: serves the backend's fetch handler and upgrades /realtime/v1 WebSockets. */
2
2
  import { createServer } from 'node:http';
3
+ import { createServer as createNetServer } from 'node:net';
3
4
  import { acceptWebSocket } from './ws.js';
5
+ /** Resolve whether a port is free to bind on `host`. */
6
+ function portIsFree(port, host) {
7
+ return new Promise((resolve) => {
8
+ const tester = createNetServer();
9
+ tester.once('error', () => resolve(false));
10
+ tester.once('listening', () => tester.close(() => resolve(true)));
11
+ tester.listen(port, host);
12
+ });
13
+ }
14
+ /**
15
+ * First free port at or after `startPort` (scanning up to `maxTries`), or null
16
+ * if none. Used so `tinbase start` skips a port that's already in use (e.g. a
17
+ * tinbase instance already running) instead of crashing with EADDRINUSE.
18
+ */
19
+ export async function findAvailablePort(startPort, host = '127.0.0.1', maxTries = 20) {
20
+ for (let p = startPort; p < startPort + maxTries; p++) {
21
+ if (await portIsFree(p, host))
22
+ return p;
23
+ }
24
+ return null;
25
+ }
4
26
  export async function serve(backend, opts = {}) {
5
27
  const host = opts.host ?? '127.0.0.1';
6
28
  const server = createServer(async (req, res) => {
@@ -29,7 +51,11 @@ export async function serve(backend, opts = {}) {
29
51
  ws.onClose = session.onClose;
30
52
  });
31
53
  await new Promise((resolve, reject) => {
32
- server.once('error', reject);
54
+ server.once('error', (err) => {
55
+ reject(err.code === 'EADDRINUSE'
56
+ ? new Error(`port ${opts.port ?? 54321} is already in use — pass --port <n> to choose another`)
57
+ : err);
58
+ });
33
59
  server.listen(opts.port ?? 54321, host, () => resolve());
34
60
  });
35
61
  const address = server.address();
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/node/server.ts"],"names":[],"mappings":"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"}
@@ -22,9 +22,19 @@ export declare class RealtimeEngine {
22
22
  /** topic → key → metas */
23
23
  private presence;
24
24
  private stopCdc;
25
+ private stopDbBroadcast;
25
26
  constructor(db: Database, jwtSecret?: string | undefined);
26
27
  /** Resolve a subscriber's access token to a role/claims context. */
27
28
  private contextFromToken;
29
+ /**
30
+ * Authorize a private channel against RLS on realtime.messages, as the
31
+ * subscriber. read = a SELECT policy lets them receive; write = an INSERT
32
+ * policy lets them broadcast. Runs in a transaction that always rolls back so
33
+ * the probe rows never persist.
34
+ */
35
+ private authorizePrivate;
36
+ /** Fan a realtime.send() database broadcast out to a topic's subscribers. */
37
+ private dispatchDbBroadcast;
28
38
  start(): Promise<void>;
29
39
  stop(): void;
30
40
  /** Attach a socket. Returns callbacks the transport must wire up. */
@@ -9,6 +9,7 @@ export class RealtimeEngine {
9
9
  /** topic → key → metas */
10
10
  presence = new Map();
11
11
  stopCdc = null;
12
+ stopDbBroadcast = null;
12
13
  constructor(db, jwtSecret) {
13
14
  this.db = db;
14
15
  this.jwtSecret = jwtSecret;
@@ -23,14 +24,78 @@ export class RealtimeEngine {
23
24
  const role = typeof claims.role === 'string' ? claims.role : 'authenticated';
24
25
  return { role, claims };
25
26
  }
27
+ /**
28
+ * Authorize a private channel against RLS on realtime.messages, as the
29
+ * subscriber. read = a SELECT policy lets them receive; write = an INSERT
30
+ * policy lets them broadcast. Runs in a transaction that always rolls back so
31
+ * the probe rows never persist.
32
+ */
33
+ async authorizePrivate(subTopic, ctx) {
34
+ const claims = ctx.claims ? JSON.stringify(ctx.claims) : '';
35
+ try {
36
+ return await this.db.engine.transaction(async (tx) => {
37
+ // seed a probe row for the topic with RLS bypassed (service_role)
38
+ await tx.query(`select set_config('role', 'service_role', true)`);
39
+ await tx.query(`insert into realtime.messages (topic, extension, event, payload, private) values ($1, 'broadcast', 'authz', '{}'::jsonb, true)`, [subTopic]);
40
+ // become the subscriber and set the topic being authorized
41
+ await tx.query(`select set_config('role', $1, true), set_config('request.jwt.claims', $2, true), set_config('realtime.topic', $3, true)`, [ctx.role, claims, subTopic]);
42
+ // read: does a SELECT policy let them see the probe row for this topic?
43
+ const rd = await tx.query(`select count(*)::int as n from realtime.messages where topic = $1`, [subTopic]);
44
+ const read = (rd.rows[0]?.n ?? 0) > 0;
45
+ // write: does an INSERT policy let them broadcast? (last query — a
46
+ // failure aborts the tx, which we roll back anyway)
47
+ let write = true;
48
+ try {
49
+ await tx.query(`insert into realtime.messages (topic, extension, event, payload, private) values ($1, 'broadcast', 'authz-w', '{}'::jsonb, true)`, [subTopic]);
50
+ }
51
+ catch {
52
+ write = false;
53
+ }
54
+ throw { __authz: { read, write } };
55
+ });
56
+ }
57
+ catch (e) {
58
+ const authz = e.__authz;
59
+ if (authz)
60
+ return authz;
61
+ // realtime schema missing / unexpected error → deny (safe default)
62
+ return { read: false, write: false };
63
+ }
64
+ }
65
+ /** Fan a realtime.send() database broadcast out to a topic's subscribers. */
66
+ dispatchDbBroadcast(msg) {
67
+ const full = `realtime:${msg.topic}`;
68
+ for (const conn of this.connections) {
69
+ const channel = conn.channels.get(full) ?? conn.channels.get(msg.topic);
70
+ if (!channel)
71
+ continue;
72
+ this.send(conn, {
73
+ topic: channel.topic,
74
+ event: 'broadcast',
75
+ payload: { type: 'broadcast', event: msg.event, payload: msg.payload },
76
+ ref: null,
77
+ });
78
+ }
79
+ }
26
80
  async start() {
27
81
  if (this.stopCdc)
28
82
  return;
29
83
  this.stopCdc = await this.db.onCdcEvent((e) => this.dispatchCdc(e));
84
+ // broadcast-from-database: realtime.send() → pg_notify → fan out to topic
85
+ this.stopDbBroadcast = await this.db.engine.listen('tinbase_realtime_broadcast', (payload) => {
86
+ try {
87
+ this.dispatchDbBroadcast(JSON.parse(payload));
88
+ }
89
+ catch {
90
+ // malformed payload — drop
91
+ }
92
+ });
30
93
  }
31
94
  stop() {
32
95
  this.stopCdc?.();
33
96
  this.stopCdc = null;
97
+ this.stopDbBroadcast?.();
98
+ this.stopDbBroadcast = null;
34
99
  for (const conn of this.connections)
35
100
  conn.socket.close(1001, 'server shutting down');
36
101
  this.connections.clear();
@@ -126,6 +191,22 @@ export class RealtimeEngine {
126
191
  }
127
192
  async handleJoin(conn, msg) {
128
193
  const config = (msg.payload?.config ?? {});
194
+ const ctx = await this.contextFromToken(msg.payload?.access_token);
195
+ const isPrivate = config.private === true;
196
+ let canBroadcast = true;
197
+ // Private channels are RLS-authorized against realtime.messages (skipped on
198
+ // subset engines with no RLS). No read policy → join is rejected.
199
+ if (isPrivate && !this.db.engine.minimalBootstrap) {
200
+ const sub = msg.topic.replace(/^realtime:/, '');
201
+ const authz = await this.authorizePrivate(sub, ctx);
202
+ if (!authz.read) {
203
+ this.reply(conn, msg, 'error', {
204
+ reason: `You do not have permissions to read from this Channel topic: ${sub}`,
205
+ });
206
+ return;
207
+ }
208
+ canBroadcast = authz.write;
209
+ }
129
210
  const bindings = [];
130
211
  for (const spec of config.postgres_changes ?? []) {
131
212
  const binding = {
@@ -163,7 +244,9 @@ export class RealtimeEngine {
163
244
  broadcastAck: config.broadcast?.ack ?? false,
164
245
  presenceKey: config.presence?.key || crypto.randomUUID(),
165
246
  presenceEnabled: config.presence?.enabled ?? true,
166
- ctx: await this.contextFromToken(msg.payload?.access_token),
247
+ ctx,
248
+ private: isPrivate,
249
+ canBroadcast,
167
250
  };
168
251
  conn.channels.set(msg.topic, channel);
169
252
  this.reply(conn, msg, 'ok', {
@@ -258,6 +341,12 @@ export class RealtimeEngine {
258
341
  const sender = conn.channels.get(msg.topic);
259
342
  if (!sender)
260
343
  return;
344
+ // private channels require INSERT (write) authorization to broadcast
345
+ if (sender.private && !sender.canBroadcast) {
346
+ if (sender.broadcastAck && msg.ref)
347
+ this.reply(conn, msg, 'error', { reason: 'unauthorized' });
348
+ return;
349
+ }
261
350
  const userPayload = msg.payload.payload;
262
351
  const binaryFrame = userPayload instanceof Uint8Array
263
352
  ? encodeUserBroadcast(msg.topic, String(msg.payload.event ?? ''), userPayload)