@edge-base/server 0.2.1 → 0.2.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 (63) hide show
  1. package/admin-build/_app/immutable/chunks/{BY07qVPA.js → 4vlsb8ej.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{D755Tqat.js → 5PDcRlfX.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B8DT4fss.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → BEYYl662.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → BKXmgPq4.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{DjOEv9M9.js → BWyDPAjM.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → BaCHY17I.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → C-DsDCNG.js} +3 -3
  9. package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → C85dMlzL.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CPdXvRUb.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → CTngeX8H.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DzXaj-Ja.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → c5iKSdWY.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → g3ZZdY-r.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → kiJ6KthZ.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → qiZXAKh-.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.BZxfavhF.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.DlsaydXO.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.D2NWN5eG.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.EMDaN3nw.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.BasqQ_o9.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.DO31Ljs7.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.DhyAy-GZ.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.CLecGWc4.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.B9kp3W4e.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.Pu_8T3RI.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.DX4z43t6.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.BKsSaxrr.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.DXNF1htN.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.VRVb0wee.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.DqZf4CtH.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.DtyxMiQG.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CloWNmTd.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.CnZWMq7_.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.DrV7XOmf.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.DV8L32OF.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.Stil2D4u.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.Zsm1e5Dc.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.CKoj2vNz.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.Ni0k5bER.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.mnqj9EbV.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.B_-z9AzT.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.yiZ72j4k.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BqykybBG.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.BDAHlhsF.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.D8Xvy0lH.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.Dddmd7_F.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -2
  53. package/src/__tests__/functions-context.test.ts +5 -5
  54. package/src/__tests__/meta-export-coverage.test.ts +1 -0
  55. package/src/lib/functions.ts +204 -397
  56. package/src/lib/internal-transport.ts +316 -0
  57. package/src/lib/plugin-migrations.ts +2 -2
  58. package/src/routes/admin.ts +7 -1
  59. package/src/routes/auth.ts +6 -12
  60. package/src/routes/storage.ts +6 -12
  61. package/src/types.ts +2 -0
  62. package/admin-build/_app/immutable/entry/start.zXCirpgY.js +0 -1
  63. package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +0 -1
@@ -25,19 +25,22 @@ import type {
25
25
  ScheduleTrigger,
26
26
  HttpTrigger,
27
27
  } from '@edge-base/shared';
28
- import { getDbDoName, getD1BindingName, callDO, shouldRouteToD1 } from './do-router.js';
28
+ import { getD1BindingName, shouldRouteToD1 } from './do-router.js';
29
29
  import { executeDoSql } from './do-sql.js';
30
30
  import { D1AuthDb, type AuthDb } from './auth-db-adapter.js';
31
- import { handleD1Request } from './d1-handler.js';
32
- import { handlePgRequest } from './postgres-handler.js';
33
- import { buildInternalHandlerContext } from './internal-request.js';
31
+ import { ensureAuthSchema } from './auth-d1.js';
34
32
  import type { Env } from '../types.js';
35
33
  import { createSignedToken } from '../routes/storage.js';
36
34
  import {
35
+ createManagedAdminUser,
37
36
  deleteManagedAdminUser,
38
37
  normalizeAdminUserUpdates,
39
38
  updateManagedAdminUser,
40
39
  } from './admin-user-management.js';
40
+ import { hashPassword } from './password.js';
41
+ import { generateId } from './uuid.js';
42
+ import { DbRef, TableRef, DefaultDbApi, HttpClient, ContextManager } from '@edge-base/core';
43
+ import { InternalHttpTransport } from './internal-transport.js';
41
44
 
42
45
  // ─── Function Context Types ───
43
46
 
@@ -48,21 +51,6 @@ export interface AuthContext {
48
51
  custom?: Record<string, unknown>;
49
52
  }
50
53
 
51
- export interface TableProxy {
52
- insert(data: Record<string, unknown>): Promise<Record<string, unknown>>;
53
- upsert(
54
- data: Record<string, unknown>,
55
- options?: { conflictTarget?: string },
56
- ): Promise<Record<string, unknown>>;
57
- update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
58
- delete(id: string): Promise<{ deleted: boolean }>;
59
- get(id: string): Promise<Record<string, unknown>>;
60
- list(options?: {
61
- limit?: number;
62
- filter?: unknown;
63
- }): Promise<{ items: Record<string, unknown>[] }>;
64
- }
65
-
66
54
  export interface AdminAuthContext {
67
55
  getUser(userId: string): Promise<Record<string, unknown>>;
68
56
  listUsers(options?: {
@@ -89,22 +77,31 @@ export interface AdminAuthContext {
89
77
  */
90
78
  export interface FunctionAdminContext {
91
79
  /** Cross-DO table access — rules bypassed, Service Key authenticated. */
92
- table(name: string): TableProxy;
80
+ table(name: string): TableRef;
93
81
  /**
94
82
  * Access a specific DB namespace instance (§5).
95
- * Replaces forTenant() aligned with Database-first architecture (#133).
83
+ * Uses the same TableRef from @edge-base/core as the client SDK,
84
+ * ensuring API parity (getList, getOne, where, orderBy, limit, etc.).
96
85
  *
97
86
  * @example
98
87
  * // Static DB
99
- * context.admin.db('shared').table('posts').list()
88
+ * context.admin.db('shared').table('posts').getList()
100
89
  * // Dynamic DB (tenant/user)
101
- * context.admin.db('workspace', 'ws-456').table('documents').list()
90
+ * context.admin.db('workspace', 'ws-456').table('documents').getList()
91
+ * // With query builder
92
+ * context.admin.db('shared').table('posts').where('status', '==', 'published').limit(10).getList()
102
93
  */
103
- db(namespace: string, id?: string): { table(name: string): TableProxy };
94
+ db(namespace: string, id?: string): DbRef;
104
95
  /** Admin user management. */
105
96
  auth: AdminAuthContext;
106
- /** Raw SQL on a DB namespace DO. */
107
- sql(
97
+ /**
98
+ * Execute raw SQL with direct D1/DO binding access — no HTTP round-trip.
99
+ * Routes directly to D1 binding or Durable Object without network overhead.
100
+ *
101
+ * @example
102
+ * const rows = await ctx.admin.sqlWithDirectD1Access('shared', undefined, 'SELECT * FROM posts WHERE status = ?', ['published']);
103
+ */
104
+ sqlWithDirectD1Access(
108
105
  namespace: string,
109
106
  id: string | undefined,
110
107
  query: string,
@@ -291,20 +288,21 @@ export interface FunctionContext {
291
288
  *
292
289
  * @example
293
290
  * // Static DB
294
- * await context.db('shared').table('posts').list()
291
+ * await context.db('shared').table('posts').getList()
295
292
  * // Dynamic DB
296
- * await context.db('workspace', 'ws-456').table('documents').list()
293
+ * await context.db('workspace', 'ws-456').table('documents').getList()
294
+ * // With query builder
295
+ * await context.db('shared').table('posts').where('status', '==', 'published').limit(10).getList()
297
296
  */
298
297
  db: FunctionAdminContext['db'];
299
298
  /**
300
- * Server-side EdgeBase admin client (§5,).
299
+ * Server-side EdgeBase admin client (§5).
301
300
  * Use context.admin.db(namespace, id?).table(name) for all DB access.
301
+ * Uses the same TableRef from @edge-base/core as the client SDK.
302
302
  *
303
303
  * @example
304
- * // Static DB
305
- * await context.admin.db('shared').table('posts').list()
306
- * // Dynamic DB
307
- * await context.admin.db('workspace', 'ws-456').table('documents').list()
304
+ * await context.admin.db('shared').table('posts').getList()
305
+ * await context.admin.db('shared').table('posts').where('userId', '==', uid).getList()
308
306
  */
309
307
  admin: FunctionAdminContext;
310
308
  /**
@@ -697,314 +695,143 @@ export function wrapMethodExport(
697
695
  };
698
696
  }
699
697
 
700
- // ─── Table Proxy (Cross-DO) ───
698
+ // ─── Admin DB Proxy (uses @edge-base/core TableRef via InternalHttpTransport) ───
701
699
 
702
- /**
703
- * Build a cross-DO table proxy.
704
- * All calls bypass access rules by using Service Key.
705
- */
706
- function buildTableProxy(
707
- tableName: string,
708
- namespace: DurableObjectNamespace,
709
- config: EdgeBaseConfig,
710
- workerUrl?: string,
711
- serviceKey?: string,
712
- env?: Env,
713
- executionCtx?: ExecutionContext,
714
- /** DB routing info (§5). If provided, overrides auto-detect. */
715
- dbTarget?: { namespace: string; id?: string; doName: string },
716
- routingOptions?: { preferDirectDo?: boolean },
717
- ): TableProxy {
718
- // Determine DO name: explicit DB target (§5) or auto-detect from config
719
- let resolvedTarget: { namespace: string; id?: string; doName: string };
720
- if (dbTarget) {
721
- resolvedTarget = dbTarget;
722
- } else {
723
- // Auto-detect namespace for this table from config (§1)
724
- let tableNamespace = 'shared';
725
- for (const [ns, dbBlock] of Object.entries(config.databases ?? {})) {
726
- if (dbBlock.tables?.[tableName]) {
727
- tableNamespace = ns;
728
- break;
729
- }
730
- }
731
- resolvedTarget = {
732
- namespace: tableNamespace,
733
- doName: getDbDoName(tableNamespace),
734
- };
735
- }
736
- const doName = resolvedTarget.doName;
737
- const headers: Record<string, string> = { 'X-DO-Name': doName };
738
- if (serviceKey) {
739
- headers['X-EdgeBase-Service-Key'] = serviceKey;
740
- }
741
- // Add internal header to bypass rules
742
- headers['X-EdgeBase-Internal'] = 'true';
743
-
744
- const buildTablePath = (id?: string, query?: URLSearchParams): string => {
745
- const base = resolvedTarget.id !== undefined
746
- ? `/api/db/${resolvedTarget.namespace}/${resolvedTarget.id}/tables/${tableName}`
747
- : `/api/db/${resolvedTarget.namespace}/tables/${tableName}`;
748
- const withId = id ? `${base}/${id}` : base;
749
- const search = query && Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
750
- return `${withId}${search}`;
751
- };
752
- const buildDirectTablePath = (id?: string, query?: URLSearchParams): string => {
753
- const base = `/tables/${tableName}`;
754
- const withId = id ? `${base}/${id}` : base;
755
- const search = query && Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
756
- return `${withId}${search}`;
757
- };
700
+ interface BuildAdminDbProxyOptions {
701
+ databaseNamespace: DurableObjectNamespace;
702
+ config: EdgeBaseConfig;
703
+ workerUrl?: string;
704
+ serviceKey?: string;
705
+ env?: Env;
706
+ executionCtx?: ExecutionContext;
707
+ preferDirectDo?: boolean;
708
+ }
758
709
 
759
- const requestViaWorker = async (
760
- method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
761
- path: string,
762
- body?: Record<string, unknown>,
763
- ): Promise<Response> => {
764
- return fetch(`${workerUrl}${path}`, {
765
- method,
766
- headers: {
767
- 'Content-Type': 'application/json',
768
- ...headers,
769
- },
770
- body: body === undefined || method === 'GET' || method === 'DELETE'
771
- ? undefined
772
- : JSON.stringify(body),
773
- });
774
- };
775
- const requestViaD1Handler = async (
776
- method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
777
- directPath: string,
778
- query?: URLSearchParams,
779
- body?: Record<string, unknown>,
780
- ): Promise<Response> => {
781
- if (!env) {
782
- throw new Error('D1 table proxy requires env.');
783
- }
710
+ // ─── Shared SQL executor — D1 direct → DO direct → HTTP fallback ───
784
711
 
785
- const request = new Request(
786
- `http://internal/api/db/${resolvedTarget.namespace}${resolvedTarget.id ? `/${resolvedTarget.id}` : ''}${buildDirectTablePath(undefined, query).replace('/tables/' + tableName, directPath)}`,
787
- {
788
- method,
789
- headers: {
790
- 'Content-Type': 'application/json',
791
- ...headers,
792
- },
793
- body: body === undefined || method === 'GET' || method === 'DELETE'
794
- ? undefined
795
- : JSON.stringify(body),
796
- },
797
- );
712
+ export interface SqlWithDirectD1AccessOptions {
713
+ env?: unknown;
714
+ config: EdgeBaseConfig;
715
+ databaseNamespace?: DurableObjectNamespace;
716
+ workerUrl?: string;
717
+ serviceKey?: string;
718
+ }
798
719
 
799
- return handleD1Request(
800
- buildInternalHandlerContext({
801
- env,
802
- request,
803
- body,
804
- executionCtx,
805
- }),
806
- resolvedTarget.namespace,
807
- tableName,
808
- directPath,
809
- );
810
- };
811
- const requestViaPgHandler = async (
812
- method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
813
- directPath: string,
814
- query?: URLSearchParams,
815
- body?: Record<string, unknown>,
816
- ): Promise<Response> => {
817
- if (!env) {
818
- throw new Error('PostgreSQL table proxy requires env.');
720
+ /**
721
+ * Execute raw SQL with the fastest available path:
722
+ * 1. D1 direct binding (no network hop)
723
+ * 2. Durable Object direct call
724
+ * 3. HTTP fallback via workerUrl
725
+ *
726
+ * Shared by buildFunctionContext, auth hooks, and storage hooks.
727
+ */
728
+ export async function executeSqlWithDirectD1Access(
729
+ opts: SqlWithDirectD1AccessOptions,
730
+ namespace: string,
731
+ id: string | undefined,
732
+ query: string,
733
+ params?: unknown[],
734
+ ): Promise<unknown[]> {
735
+ if (opts.env) {
736
+ const dbBlock = opts.config.databases?.[namespace];
737
+ const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
738
+ if (isDynamicNamespace && !id) {
739
+ throw new Error(`admin.sqlWithDirectD1Access() requires an id for dynamic namespace '${namespace}'.`);
819
740
  }
820
741
 
821
- const request = new Request(
822
- `http://internal/api/db/${resolvedTarget.namespace}${resolvedTarget.id ? `/${resolvedTarget.id}` : ''}${buildDirectTablePath(undefined, query).replace('/tables/' + tableName, directPath)}`,
823
- {
824
- method,
825
- headers: {
826
- 'Content-Type': 'application/json',
827
- ...headers,
828
- },
829
- body: body === undefined || method === 'GET' || method === 'DELETE'
830
- ? undefined
831
- : JSON.stringify(body),
832
- },
833
- );
834
-
835
- return handlePgRequest(
836
- buildInternalHandlerContext({
837
- env,
838
- request,
839
- body,
840
- executionCtx,
841
- }),
842
- resolvedTarget.namespace,
843
- tableName,
844
- directPath,
845
- );
846
- };
847
- const requestViaDirectDo = async (
848
- method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
849
- directPath: string,
850
- body?: Record<string, unknown>,
851
- ): Promise<Response> => {
852
- const res = await callDO(namespace, doName, directPath, {
853
- method,
854
- body,
855
- headers,
856
- });
857
-
858
- if (!resolvedTarget.id || res.status !== 201) {
859
- return res;
742
+ if (!id && shouldRouteToD1(namespace, opts.config)) {
743
+ const bindingName = getD1BindingName(namespace);
744
+ const d1 = (opts.env as Record<string, unknown>)[bindingName] as D1Database | undefined;
745
+ if (!d1) {
746
+ throw new Error(`D1 binding '${bindingName}' not found.`);
747
+ }
748
+ try {
749
+ const stmt = d1.prepare(query);
750
+ const bound = params && params.length > 0 ? stmt.bind(...params) : stmt;
751
+ const result = await bound.all();
752
+ return (result.results ?? []) as unknown[];
753
+ } catch (error) {
754
+ const message = error instanceof Error ? error.message : 'SQL execution failed';
755
+ throw new Error(message);
756
+ }
860
757
  }
861
758
 
862
- const createPayload = await res.clone().json().catch(() => null) as
863
- | { needsCreate?: boolean }
864
- | null;
865
- if (!createPayload?.needsCreate) {
866
- return res;
759
+ if (opts.databaseNamespace) {
760
+ return executeDoSql({
761
+ databaseNamespace: opts.databaseNamespace,
762
+ namespace,
763
+ id,
764
+ query,
765
+ params: params ?? [],
766
+ internal: true,
767
+ });
867
768
  }
769
+ }
868
770
 
869
- return callDO(namespace, doName, directPath, {
870
- method,
871
- body,
771
+ if (opts.workerUrl && opts.serviceKey) {
772
+ const res = await fetch(`${opts.workerUrl}/api/sql`, {
773
+ method: 'POST',
872
774
  headers: {
873
- ...headers,
874
- 'X-DO-Create-Authorized': '1',
775
+ 'Content-Type': 'application/json',
776
+ 'X-EdgeBase-Service-Key': opts.serviceKey,
875
777
  },
778
+ body: JSON.stringify({ namespace, id, sql: query, params: params ?? [] }),
876
779
  });
877
- };
878
- const requestTable = async (
879
- method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
880
- id?: string,
881
- body?: Record<string, unknown>,
882
- query?: URLSearchParams,
883
- ): Promise<Response> => {
884
- const directPath = buildDirectTablePath(id);
885
- const directPathWithQuery = buildDirectTablePath(id, query);
886
- const provider = config.databases?.[resolvedTarget.namespace]?.provider;
887
-
888
- if (!routingOptions?.preferDirectDo && shouldRouteToD1(resolvedTarget.namespace, config) && env) {
889
- return requestViaD1Handler(method, directPath, query, body);
890
- }
891
- if ((provider === 'neon' || provider === 'postgres') && env) {
892
- return requestViaPgHandler(method, directPath, query, body);
893
- }
894
- if (env) {
895
- return requestViaDirectDo(method, directPathWithQuery, body);
896
- }
897
- if (workerUrl) {
898
- return requestViaWorker(method, buildTablePath(id, query), body);
899
- }
900
- return requestViaDirectDo(method, directPathWithQuery, body);
901
- };
902
-
903
- const insert = async (data: Record<string, unknown>): Promise<Record<string, unknown>> => {
904
- const res = await requestTable('POST', undefined, data);
905
780
  if (!res.ok) {
906
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
907
- throw new Error(`Cross-DO insert failed: ${err.message || res.status}`);
908
- }
909
- return (await res.json()) as Record<string, unknown>;
910
- };
911
-
912
- const upsert = async (
913
- data: Record<string, unknown>,
914
- options?: { conflictTarget?: string },
915
- ): Promise<Record<string, unknown>> => {
916
- const query = new URLSearchParams();
917
- query.set('upsert', 'true');
918
- if (options?.conflictTarget) {
919
- query.set('conflictTarget', options.conflictTarget);
920
- }
921
-
922
- const res = await requestTable('POST', undefined, data, query);
923
- if (!res.ok) {
924
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
925
- throw new Error(`Cross-DO upsert failed: ${err.message || res.status}`);
926
- }
927
- return (await res.json()) as Record<string, unknown>;
928
- };
929
-
930
- const update = async (
931
- id: string,
932
- data: Record<string, unknown>,
933
- ): Promise<Record<string, unknown>> => {
934
- const res = await requestTable('PATCH', id, data);
935
- if (!res.ok) {
936
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
937
- throw new Error(`Cross-DO update failed: ${err.message || res.status}`);
938
- }
939
- return (await res.json()) as Record<string, unknown>;
940
- };
941
-
942
- const del = async (id: string): Promise<{ deleted: boolean }> => {
943
- const res = await requestTable('DELETE', id);
944
- if (!res.ok) {
945
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
946
- throw new Error(`Cross-DO delete failed: ${err.message || res.status}`);
947
- }
948
- return (await res.json()) as { deleted: boolean };
949
- };
950
-
951
- const get = async (id: string): Promise<Record<string, unknown>> => {
952
- const res = await requestTable('GET', id);
953
- if (!res.ok) {
954
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
955
- throw new Error(`Cross-DO get failed: ${err.message || res.status}`);
956
- }
957
- return (await res.json()) as Record<string, unknown>;
958
- };
959
-
960
- const list = async (listOptions?: {
961
- limit?: number;
962
- filter?: unknown;
963
- }): Promise<{ items: Record<string, unknown>[] }> => {
964
- const query = new URLSearchParams();
965
- if (listOptions?.limit) query.set('limit', String(listOptions.limit));
966
- if (listOptions?.filter) query.set('filter', JSON.stringify(listOptions.filter));
967
-
968
- const res = await requestTable('GET', undefined, undefined, query);
969
- if (!res.ok) {
970
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
971
- throw new Error(`Cross-DO list failed: ${err.message || res.status}`);
781
+ const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as { message: string };
782
+ throw new Error(err.message);
972
783
  }
973
- return (await res.json()) as { items: Record<string, unknown>[] };
974
- };
784
+ const data = (await res.json()) as { rows?: unknown[]; items?: unknown[]; results?: unknown[] };
785
+ if (Array.isArray(data.rows)) return data.rows;
786
+ if (Array.isArray(data.items)) return data.items;
787
+ if (Array.isArray(data.results)) return data.results;
788
+ return [];
789
+ }
975
790
 
976
- return { insert, upsert, update, delete: del, get, list };
977
- }
978
-
979
- interface BuildAdminDbProxyOptions {
980
- databaseNamespace: DurableObjectNamespace;
981
- config: EdgeBaseConfig;
982
- workerUrl?: string;
983
- serviceKey?: string;
984
- env?: Env;
985
- executionCtx?: ExecutionContext;
986
- preferDirectDo?: boolean;
791
+ throw new Error(
792
+ 'admin.sqlWithDirectD1Access() requires env or workerUrl.',
793
+ );
987
794
  }
988
795
 
796
+ /**
797
+ * Build the admin DB proxy that returns real DbRef/TableRef instances
798
+ * from @edge-base/core, routed through InternalHttpTransport for
799
+ * direct D1/PG/DO access (no HTTP round-trip).
800
+ */
989
801
  export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAdminContext['db'] {
990
- return (namespace: string, id?: string) => {
991
- const doName = getDbDoName(namespace, id);
992
-
993
- return {
994
- table(tableName: string): TableProxy {
995
- return buildTableProxy(
996
- tableName,
997
- options.databaseNamespace,
998
- options.config,
999
- options.workerUrl,
1000
- options.serviceKey,
1001
- options.env,
1002
- options.executionCtx,
1003
- { namespace, id, doName },
1004
- { preferDirectDo: options.preferDirectDo },
1005
- );
1006
- },
1007
- };
802
+ // Create HttpClient for sql() tagged template support on TableRef.
803
+ // Only available when workerUrl is set (routes through /api/sql endpoint).
804
+ let httpClient: HttpClient | undefined;
805
+ if (options.workerUrl) {
806
+ httpClient = new HttpClient({
807
+ baseUrl: options.workerUrl,
808
+ serviceKey: options.serviceKey,
809
+ contextManager: new ContextManager(),
810
+ });
811
+ }
812
+
813
+ return (namespace: string, id?: string): DbRef => {
814
+ // Create a per-DbRef transport with explicit dbContext so that
815
+ // path parsing is unambiguous even when instanceId === 'tables'.
816
+ const transport = new InternalHttpTransport({
817
+ databaseNamespace: options.databaseNamespace,
818
+ config: options.config,
819
+ workerUrl: options.workerUrl,
820
+ serviceKey: options.serviceKey,
821
+ env: options.env,
822
+ executionCtx: options.executionCtx,
823
+ preferDirectDo: options.preferDirectDo,
824
+ dbContext: { namespace, instanceId: id },
825
+ });
826
+ const dbApi = new DefaultDbApi(transport);
827
+ return new DbRef(
828
+ dbApi,
829
+ namespace,
830
+ id,
831
+ undefined, // databaseLiveClient — not available server-side
832
+ undefined, // filterMatchFn
833
+ httpClient, // enables table().sql`...` tagged template
834
+ );
1008
835
  };
1009
836
  }
1010
837
 
@@ -1024,7 +851,7 @@ interface AdminAuthOptions {
1024
851
  /**
1025
852
  * Build admin auth context for App Functions.
1026
853
  * Uses AUTH_DB D1 directly for all operations (D1-first architecture).
1027
- * Cross-shard operations (listUsers, createUser) also available via Worker HTTP relay
854
+ * Cross-shard operations (listUsers) also available via Worker HTTP relay
1028
855
  * when workerUrl is provided.
1029
856
  */
1030
857
  export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthContext {
@@ -1089,10 +916,49 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
1089
916
  displayName?: string;
1090
917
  role?: string;
1091
918
  }): Promise<Record<string, unknown>> {
919
+ // Direct D1 path — works without service key (same as updateUser/deleteUser)
920
+ if (d1Database) {
921
+ // Input validation (mirrors routes/admin-auth.ts guards)
922
+ if (!data.email || typeof data.email !== 'string') throw new Error('Email and password are required.');
923
+ if (!data.password || typeof data.password !== 'string') throw new Error('Email and password are required.');
924
+ const email = data.email.trim().toLowerCase();
925
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
926
+ throw new Error('Invalid email format.');
927
+ }
928
+ if (data.password.length < 8) throw new Error('Password must be at least 8 characters.');
929
+ if (data.password.length > 256) throw new Error('Password must not exceed 256 characters.');
930
+ if (data.displayName && data.displayName.length > 200) {
931
+ throw new Error('Display name must not exceed 200 characters.');
932
+ }
933
+ // Role validation (mirrors normalizeOptionalRole in routes/admin-auth.ts)
934
+ let role = 'user';
935
+ if (data.role !== undefined) {
936
+ if (typeof data.role !== 'string') throw new Error('Role must be a non-empty string.');
937
+ const trimmed = data.role.trim();
938
+ if (!trimmed) throw new Error('Role must be a non-empty string.');
939
+ if (trimmed.length > 100) throw new Error('Role must not exceed 100 characters.');
940
+ role = trimmed;
941
+ }
942
+
943
+ const db = new D1AuthDb(d1Database);
944
+ // Ensure auth tables exist (critical for fresh databases)
945
+ await ensureAuthSchema(db);
946
+ const user = await createManagedAdminUser(
947
+ db,
948
+ {
949
+ userId: generateId(),
950
+ email,
951
+ passwordHash: await hashPassword(data.password),
952
+ displayName: data.displayName,
953
+ role,
954
+ verified: true,
955
+ },
956
+ { kv: kvNamespace },
957
+ );
958
+ return authService.sanitizeUser(user, { includeAppMetadata: true });
959
+ }
1092
960
  if (workerUrl && serviceKey) {
1093
- // HTTP relay: POST /api/auth/admin/users → Worker → D1
1094
- // createUser has complex side effects (email index, _users_public, etc.)
1095
- // so it routes through the admin route which handles the full flow.
961
+ // HTTP relay fallback: POST /api/auth/admin/users → Worker → D1
1096
962
  const res = await fetch(`${workerUrl}/api/auth/admin/users`, {
1097
963
  method: 'POST',
1098
964
  headers: {
@@ -1111,7 +977,7 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
1111
977
  return result.user;
1112
978
  }
1113
979
  throw new Error(
1114
- 'admin.auth.createUser() is not available in this context (requires workerUrl). ' +
980
+ 'admin.auth.createUser() is not available in this context (requires D1 or workerUrl). ' +
1115
981
  'Pass workerUrl to buildFunctionContext(), or use the external SDK.',
1116
982
  );
1117
983
  },
@@ -1223,77 +1089,18 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1223
1089
  // ─── context.admin.db(namespace, id) — DB-first tenant access (§5) ───
1224
1090
  db: adminDb,
1225
1091
  auth: adminAuthContext,
1226
- async sql(
1227
- namespace: string,
1228
- id: string | undefined,
1229
- query: string,
1230
- params?: unknown[],
1231
- ): Promise<unknown[]> {
1232
- if (options.env) {
1233
- const dbBlock = options.config.databases?.[namespace];
1234
- const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
1235
- if (isDynamicNamespace && !id) {
1236
- throw new Error(`admin.sql() requires an id for dynamic namespace '${namespace}'.`);
1237
- }
1238
-
1239
- if (!id && shouldRouteToD1(namespace, options.config)) {
1240
- const bindingName = getD1BindingName(namespace);
1241
- const d1 = (options.env as unknown as Record<string, unknown>)[bindingName] as D1Database | undefined;
1242
- if (!d1) {
1243
- throw new Error(`D1 binding '${bindingName}' not found.`);
1244
- }
1245
- try {
1246
- const stmt = d1.prepare(query);
1247
- const bound = params && params.length > 0 ? stmt.bind(...params) : stmt;
1248
- const result = await bound.all();
1249
- const rows = (result.results ?? []) as unknown[];
1250
- return rows;
1251
- } catch (error) {
1252
- const message = error instanceof Error ? error.message : 'SQL execution failed';
1253
- throw new Error(message);
1254
- }
1255
- }
1256
-
1257
- return executeDoSql({
1092
+ // ─── Direct D1/DO SQL — delegates to shared executor ───
1093
+ sqlWithDirectD1Access: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
1094
+ executeSqlWithDirectD1Access(
1095
+ {
1096
+ env: options.env,
1097
+ config: options.config,
1258
1098
  databaseNamespace: options.databaseNamespace,
1259
- namespace,
1260
- id,
1261
- query,
1262
- params: params ?? [],
1263
- internal: true,
1264
- });
1265
- }
1266
-
1267
- if (options.workerUrl && options.serviceKey) {
1268
- // HTTP route: POST /api/sql → Worker → DatabaseDO (§11)
1269
- const res = await fetch(`${options.workerUrl}/api/sql`, {
1270
- method: 'POST',
1271
- headers: {
1272
- 'Content-Type': 'application/json',
1273
- 'X-EdgeBase-Service-Key': options.serviceKey,
1274
- },
1275
- body: JSON.stringify({ namespace, id, sql: query, params: params ?? [] }),
1276
- });
1277
- if (!res.ok) {
1278
- const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as {
1279
- message: string;
1280
- };
1281
- throw new Error(err.message);
1282
- }
1283
- const data = (await res.json()) as {
1284
- rows?: unknown[];
1285
- items?: unknown[];
1286
- results?: unknown[];
1287
- };
1288
- if (Array.isArray(data.rows)) return data.rows;
1289
- if (Array.isArray(data.items)) return data.items;
1290
- if (Array.isArray(data.results)) return data.results;
1291
- return [];
1292
- }
1293
- throw new Error(
1294
- 'admin.sql() requires workerUrl. Pass workerUrl to buildFunctionContext(), or use the external SDK.',
1295
- );
1296
- },
1099
+ workerUrl: options.workerUrl,
1100
+ serviceKey: options.serviceKey,
1101
+ },
1102
+ namespace, id, query, params,
1103
+ ),
1297
1104
  async broadcast(
1298
1105
  channel: string,
1299
1106
  event: string,