@edge-base/server 0.2.8 → 0.3.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 (78) hide show
  1. package/admin-build/_app/immutable/chunks/{zl2AUKMP.js → BSIzoJ5I.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{j4jxnAKj.js → BeYswbvA.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{DcVb45Ds.js → BkrC_6Ss.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{fPy6xmgG.js → BunyH6GE.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{B9efkx2V.js → C-zXt-cq.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{DYtrHeVQ.js → C1Mycuvd.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{Bt4AyT3o.js → Cek51lkE.js} +3 -3
  8. package/admin-build/_app/immutable/chunks/{D8aeTKry.js → D0mNYcxN.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/D3hdqsfp.js +1 -0
  10. package/admin-build/_app/immutable/chunks/{DPgR4-0v.js → DF40xjpj.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{CTRjWhGs.js → DOOhHO-v.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/DdjFeYwG.js +1 -0
  13. package/admin-build/_app/immutable/chunks/{CwyE59Yt.js → Dvlxm2Iq.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{BMXWUTG-.js → Xm1dHMTE.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{DGAHkap7.js → aPulNooa.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{CMYgGhZR.js → dZsKGnWZ.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.Cmz0WjMl.js → app.j1WHBlZX.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.BgMeGq-q.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.y6D_QyUb.js → 0.Oo7ZpbR-.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.CndRxhbH.js → 1.zgMnKHns.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.CdA5FmXy.js → 10.u5mkRiUe.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.DG8SzMp_.js → 11.9ynAgaxV.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.CvmQqpFa.js → 12.M_EqzJ6s.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.BbGNdswT.js → 13.D_NQ9q60.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.CZKsN7-O.js → 14.-vENIJ2w.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.A7-CYgkG.js → 15.DoMCZhGv.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.hgJT9H-x.js → 16.6BmnzScq.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.DkWZbcN2.js → 17.q6QQ9N_J.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.sX3Fb5gh.js → 18.DUhJ6dKj.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.VAZUW-1K.js → 19.CvkvPlGP.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.DkIKxacG.js → 20.cACSbRIl.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.DZ7G8rMF.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.BDaHvtaw.js → 22.BsQ6xeKr.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.BVRzw_pD.js → 23.CNW3mLz5.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CVhSJyG0.js → 24.CReqtEOy.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.Bme-9bZn.js → 25.BuxVhqlb.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.Dsx7RIIs.js → 26.C-LzPf62.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.DMGQnzFM.js → 27.Dl5RL_tB.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.GGwFmEhZ.js → 28.CWfSQgjH.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.Dnghr0nk.js → 29.wUSfh2eQ.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.Cg7zZJP1.js → 3.D_laBXeK.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.C0J24z3I.js → 30.DcMhZyQ0.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.MdxFI8v6.js → 31.CSwhNsNi.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.DCAOVzGE.js → 4.Cq28vPI2.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.DzUQ-cTc.js → 5.BTT9B3oI.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.CptBYTVj.js → 6.iCmmzXZp.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.DfeeQ0Rg.js → 7.qY5v7qqJ.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.CIcvctW7.js → 8.C6p4RvgK.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.QKrvq4RA.js → 9.f548N7zh.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/openapi.json +305 -3
  53. package/package.json +3 -3
  54. package/src/__tests__/auth-token.test.ts +142 -0
  55. package/src/__tests__/email-provider.test.ts +82 -0
  56. package/src/__tests__/functions-context.test.ts +87 -0
  57. package/src/__tests__/functions-d1-proxy.test.ts +70 -0
  58. package/src/__tests__/functions-route.test.ts +15 -0
  59. package/src/__tests__/room-access-policy.test.ts +26 -0
  60. package/src/__tests__/room-handler-context.test.ts +2 -0
  61. package/src/__tests__/smoke-skip-report.test.ts +1 -1
  62. package/src/durable-objects/room-runtime-base.ts +94 -11
  63. package/src/durable-objects/rooms-do.ts +4 -1
  64. package/src/lib/auth-d1-service.ts +37 -7
  65. package/src/lib/auth-token.ts +66 -0
  66. package/src/lib/email-provider.ts +115 -5
  67. package/src/lib/functions.ts +91 -1
  68. package/src/lib/internal-transport.ts +15 -1
  69. package/src/lib/jwt.ts +2 -0
  70. package/src/routes/admin.ts +8 -1
  71. package/src/routes/auth.ts +111 -19
  72. package/src/routes/room.ts +36 -2
  73. package/src/routes/storage.ts +446 -62
  74. package/src/types.ts +10 -0
  75. package/admin-build/_app/immutable/chunks/CKVjMXZi.js +0 -1
  76. package/admin-build/_app/immutable/chunks/Djnkhy-S.js +0 -1
  77. package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +0 -1
  78. package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +0 -1
@@ -0,0 +1,66 @@
1
+ const encoder = new TextEncoder();
2
+
3
+ function toHex(value: ArrayBuffer): string {
4
+ return Array.from(new Uint8Array(value))
5
+ .map((byte) => byte.toString(16).padStart(2, '0'))
6
+ .join('');
7
+ }
8
+
9
+ function timingSafeStringEqual(a: string, b: string): boolean {
10
+ if (a.length !== b.length) return false;
11
+ let result = 0;
12
+ for (let i = 0; i < a.length; i += 1) {
13
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
14
+ }
15
+ return result === 0;
16
+ }
17
+
18
+ async function hmacSha256Hex(key: string, value: string): Promise<string> {
19
+ const cryptoKey = await crypto.subtle.importKey(
20
+ 'raw',
21
+ encoder.encode(key),
22
+ { name: 'HMAC', hash: 'SHA-256' },
23
+ false,
24
+ ['sign'],
25
+ );
26
+ const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(value));
27
+ return toHex(signature);
28
+ }
29
+
30
+ export async function hashAuthSecret(secret: string): Promise<string> {
31
+ const digest = await crypto.subtle.digest('SHA-256', encoder.encode(secret));
32
+ return `sha256:${toHex(digest)}`;
33
+ }
34
+
35
+ export async function authSecretLookupKeys(secret: string): Promise<string[]> {
36
+ const hashed = await hashAuthSecret(secret);
37
+ return secret.startsWith('sha256:') ? [secret] : [hashed, secret];
38
+ }
39
+
40
+ export async function verifyAuthSecret(secret: string, storedHash: string): Promise<boolean> {
41
+ const candidate = await hashAuthSecret(secret);
42
+ return timingSafeStringEqual(candidate, storedHash);
43
+ }
44
+
45
+ export async function hashOtpSecret(otp: string, serverSecret: string): Promise<string> {
46
+ const digest = await hmacSha256Hex(serverSecret, `edgebase:auth:otp:${otp}`);
47
+ return `hmac-sha256:${digest}`;
48
+ }
49
+
50
+ export async function verifyOtpSecret(
51
+ otp: string,
52
+ storedHash: string,
53
+ serverSecret: string,
54
+ ): Promise<boolean> {
55
+ if (storedHash.startsWith('hmac-sha256:')) {
56
+ const candidate = await hashOtpSecret(otp, serverSecret);
57
+ return timingSafeStringEqual(candidate, storedHash);
58
+ }
59
+
60
+ // Backward compatibility for short-lived OTPs stored before HMAC hashing.
61
+ if (storedHash.startsWith('sha256:')) {
62
+ return verifyAuthSecret(otp, storedHash);
63
+ }
64
+
65
+ return false;
66
+ }
@@ -4,7 +4,7 @@
4
4
  * Workers cannot use SMTP directly. Instead, we use HTTP REST API-based
5
5
  * external email services via a common interface.
6
6
  *
7
- * Supported: Resend (default), SendGrid, Mailgun, AWS SES
7
+ * Supported: Resend (default), SendGrid, Mailgun, AWS SES, Cloudflare Email Service
8
8
  */
9
9
 
10
10
  // ─── Interface ───
@@ -26,6 +26,24 @@ export interface EmailProvider {
26
26
 
27
27
  interface EmailProviderEnv {
28
28
  EDGEBASE_EMAIL_API_URL?: string;
29
+ EDGEBASE_EMAIL_CLOUDFLARE_API_TOKEN?: string;
30
+ EDGEBASE_EMAIL_CLOUDFLARE_ACCOUNT_ID?: string;
31
+ EDGEBASE_EMAIL_CLOUDFLARE_BINDING?: string;
32
+ EMAIL?: CloudflareSendEmailBinding;
33
+ }
34
+
35
+ interface CloudflareSendEmailBinding {
36
+ send(message: {
37
+ to: string;
38
+ from: string;
39
+ subject: string;
40
+ html?: string;
41
+ text?: string;
42
+ }): Promise<{ messageId?: string }>;
43
+ }
44
+
45
+ function isCloudflareSendEmailBinding(value: unknown): value is CloudflareSendEmailBinding {
46
+ return !!value && typeof value === 'object' && typeof (value as { send?: unknown }).send === 'function';
29
47
  }
30
48
 
31
49
  const encoder = new TextEncoder();
@@ -331,14 +349,83 @@ export class SESProvider implements EmailProvider {
331
349
  }
332
350
  }
333
351
 
352
+ // ─── Cloudflare Email Service Provider ───
353
+
354
+ export class CloudflareEmailProvider implements EmailProvider {
355
+ private apiBaseUrl: string;
356
+
357
+ constructor(
358
+ private options: {
359
+ from: string;
360
+ apiKey?: string;
361
+ accountId?: string;
362
+ binding?: CloudflareSendEmailBinding;
363
+ apiBaseUrl?: string;
364
+ },
365
+ ) {
366
+ this.apiBaseUrl = options.apiBaseUrl?.replace(/\/$/, '') ?? 'https://api.cloudflare.com/client/v4';
367
+ }
368
+
369
+ async send(options: EmailSendOptions): Promise<EmailSendResult> {
370
+ if (this.options.binding) {
371
+ try {
372
+ const result = await this.options.binding.send({
373
+ to: options.to,
374
+ from: this.options.from,
375
+ subject: options.subject,
376
+ html: options.html,
377
+ });
378
+ return { success: true, messageId: result.messageId };
379
+ } catch (err) {
380
+ console.error('[EmailProvider:Cloudflare] Workers binding failed:', err);
381
+ return { success: false };
382
+ }
383
+ }
384
+
385
+ if (!this.options.apiKey || !this.options.accountId) {
386
+ console.error(
387
+ '[EmailProvider:Cloudflare] accountId and apiKey are required when a Workers send_email binding is not available.',
388
+ );
389
+ return { success: false };
390
+ }
391
+
392
+ const resp = await fetch(
393
+ `${this.apiBaseUrl}/accounts/${encodeURIComponent(this.options.accountId)}/email/sending/send`,
394
+ {
395
+ method: 'POST',
396
+ headers: {
397
+ Authorization: `Bearer ${this.options.apiKey}`,
398
+ 'Content-Type': 'application/json',
399
+ },
400
+ body: JSON.stringify({
401
+ to: options.to,
402
+ from: this.options.from,
403
+ subject: options.subject,
404
+ html: options.html,
405
+ }),
406
+ },
407
+ );
408
+
409
+ if (!resp.ok) {
410
+ const text = await resp.text();
411
+ console.error('[EmailProvider:Cloudflare] Failed:', resp.status, text);
412
+ return { success: false };
413
+ }
414
+
415
+ return { success: true };
416
+ }
417
+ }
418
+
334
419
  // ─── Factory ───
335
420
 
336
421
  export interface EmailConfig {
337
- provider: 'resend' | 'sendgrid' | 'mailgun' | 'ses';
338
- apiKey: string; // SES uses accessKeyId:secretAccessKey[:sessionToken]
422
+ provider: 'resend' | 'sendgrid' | 'mailgun' | 'ses' | 'cloudflare';
423
+ apiKey?: string; // SES uses accessKeyId:secretAccessKey[:sessionToken]
339
424
  from: string;
340
425
  domain?: string; // Mailgun domain
341
426
  region?: string; // SES region
427
+ accountId?: string; // Cloudflare REST API account ID
428
+ binding?: string; // Cloudflare Workers send_email binding name
342
429
  }
343
430
 
344
431
  /**
@@ -358,22 +445,45 @@ export function createEmailProvider(
358
445
  return null;
359
446
  }
360
447
 
361
- if (!config.apiKey || !config.from) {
362
- console.warn('[EmailProvider] apiKey and from are required. Email features disabled.');
448
+ if (!config.from) {
449
+ console.warn('[EmailProvider] from is required. Email features disabled.');
363
450
  return null;
364
451
  }
365
452
 
366
453
  switch (config.provider) {
454
+ case 'cloudflare': {
455
+ const bindingName = config.binding
456
+ ?? env?.EDGEBASE_EMAIL_CLOUDFLARE_BINDING?.trim()
457
+ ?? 'EMAIL';
458
+ const envRecord = env as Record<string, unknown> | undefined;
459
+ const bindingCandidate = envRecord?.[bindingName];
460
+ const binding = isCloudflareSendEmailBinding(bindingCandidate)
461
+ ? bindingCandidate
462
+ : undefined;
463
+ return new CloudflareEmailProvider({
464
+ from: config.from,
465
+ apiKey: config.apiKey ?? env?.EDGEBASE_EMAIL_CLOUDFLARE_API_TOKEN,
466
+ accountId: config.accountId ?? env?.EDGEBASE_EMAIL_CLOUDFLARE_ACCOUNT_ID,
467
+ binding,
468
+ });
469
+ }
367
470
  case 'resend':
471
+ if (!config.apiKey) break;
368
472
  return new ResendProvider(config.apiKey, config.from);
369
473
  case 'sendgrid':
474
+ if (!config.apiKey) break;
370
475
  return new SendGridProvider(config.apiKey, config.from);
371
476
  case 'mailgun':
477
+ if (!config.apiKey) break;
372
478
  return new MailgunProvider(config.apiKey, config.from, config.domain);
373
479
  case 'ses':
480
+ if (!config.apiKey) break;
374
481
  return new SESProvider(config.apiKey, config.from, config.region);
375
482
  default:
376
483
  console.warn(`[EmailProvider] Unknown provider: ${config.provider}. Email features disabled.`);
377
484
  return null;
378
485
  }
486
+
487
+ console.warn('[EmailProvider] apiKey and from are required. Email features disabled.');
488
+ return null;
379
489
  }
@@ -41,6 +41,7 @@ import { generateId } from './uuid.js';
41
41
  import { DbRef, TableRef, DefaultDbApi, HttpClient, ContextManager } from '@edge-base/core';
42
42
  import { InternalHttpTransport } from './internal-transport.js';
43
43
  import { executeProviderAwareSql } from './provider-aware-sql.js';
44
+ import { createEmailProvider } from './email-provider.js';
44
45
 
45
46
  // ─── Function Context Types ───
46
47
 
@@ -69,6 +70,15 @@ export interface AdminAuthContext {
69
70
  revokeAllSessions(userId: string): Promise<void>;
70
71
  }
71
72
 
73
+ export interface FunctionEmailProxy {
74
+ send(options: {
75
+ to: string;
76
+ subject: string;
77
+ html?: string;
78
+ text?: string;
79
+ }): Promise<{ success: boolean; messageId?: string }>;
80
+ }
81
+
72
82
  /**
73
83
  * AdminEdgeBase-shaped context object injected as context.admin.
74
84
  *
@@ -186,6 +196,8 @@ export interface FunctionPushProxy {
186
196
 
187
197
  /** Storage proxy for App Functions — wraps R2Bucket with convenience methods. */
188
198
  export interface FunctionStorageProxy {
199
+ /** Return a proxy scoped to a named storage bucket. */
200
+ bucket(bucket: string): FunctionStorageProxy;
189
201
  put(
190
202
  key: string,
191
203
  value: ReadableStream | ArrayBuffer | string,
@@ -199,6 +211,10 @@ export interface FunctionStorageProxy {
199
211
  } | null>;
200
212
  delete(key: string): Promise<void>;
201
213
  getSignedUrl(key: string, options?: { expiresIn?: number }): Promise<string>;
214
+ getSignedUploadUrl(
215
+ key: string,
216
+ options?: { expiresIn?: number; maxBytes?: number | null },
217
+ ): Promise<{ url: string; expiresAt: string; maxBytes: number | null }>;
202
218
  list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{
203
219
  keys: Array<{ key: string; size: number; contentType: string }>;
204
220
  cursor?: string;
@@ -357,12 +373,29 @@ export interface FunctionContext {
357
373
  data?: { before?: Record<string, unknown>; after?: Record<string, unknown> };
358
374
  before?: Record<string, unknown>;
359
375
  after?: Record<string, unknown>;
376
+ /** Configured server-side email sender, when an EdgeBase email provider is available. */
377
+ email?: FunctionEmailProxy;
378
+ /** Environment variables, secrets, and bindings available to the Worker. */
379
+ env?: Record<string, unknown>;
360
380
  storage?: FunctionStorageProxy; // Available when R2 binding present
361
381
  analytics?: unknown; // AnalyticsAdapter when ANALYTICS_APP binding present
362
382
  /** Plugin-specific config from edgebase.config.ts plugins section. */
363
383
  pluginConfig?: Record<string, unknown>;
364
384
  }
365
385
 
386
+ function escapeEmailHtml(value: string): string {
387
+ return value
388
+ .replace(/&/g, '&amp;')
389
+ .replace(/</g, '&lt;')
390
+ .replace(/>/g, '&gt;')
391
+ .replace(/"/g, '&quot;')
392
+ .replace(/'/g, '&#39;');
393
+ }
394
+
395
+ function textEmailToHtml(text: string): string {
396
+ return `<pre style="font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; white-space: pre-wrap;">${escapeEmailHtml(text)}</pre>`;
397
+ }
398
+
366
399
  // ─── Function Registry ───
367
400
 
368
401
  /**
@@ -693,6 +726,19 @@ export function wrapMethodExport(
693
726
  method: string,
694
727
  runtimeTrigger?: { path?: string },
695
728
  ): FunctionDefinition {
729
+ if (
730
+ method === '*' &&
731
+ handler &&
732
+ typeof handler === 'object' &&
733
+ 'handler' in handler &&
734
+ 'trigger' in handler &&
735
+ handler.trigger &&
736
+ typeof handler.trigger === 'object' &&
737
+ 'type' in handler.trigger
738
+ ) {
739
+ return handler as FunctionDefinition;
740
+ }
741
+
696
742
  // handler can be: raw function, or { handler, captcha? } from defineFunction()
697
743
  let fn: (ctx: unknown) => Promise<unknown>;
698
744
  let captcha: boolean | undefined;
@@ -1096,6 +1142,19 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1096
1142
  executionCtx: options.executionCtx,
1097
1143
  preferDirectDo: options.preferDirectDoDb,
1098
1144
  });
1145
+ const emailProvider = createEmailProvider(options.config.email, options.env);
1146
+ const email: FunctionEmailProxy | undefined = emailProvider
1147
+ ? {
1148
+ async send(message) {
1149
+ const to = message.to.trim();
1150
+ const subject = message.subject.trim();
1151
+ if (!to) throw new Error('email.send() requires a recipient.');
1152
+ if (!subject) throw new Error('email.send() requires a subject.');
1153
+ const html = message.html ?? textEmailToHtml(message.text ?? '');
1154
+ return emailProvider.send({ to, subject, html });
1155
+ },
1156
+ }
1157
+ : undefined;
1099
1158
  const sqlProviderAware = (
1100
1159
  namespace: string,
1101
1160
  id: string | undefined,
@@ -1282,6 +1341,8 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
1282
1341
  auth: options.auth,
1283
1342
  db: admin.db,
1284
1343
  admin,
1344
+ ...(options.env ? { env: options.env as unknown as Record<string, unknown> } : {}),
1345
+ ...(email ? { email } : {}),
1285
1346
  params: options.params ?? {},
1286
1347
  };
1287
1348
 
@@ -1826,6 +1887,10 @@ export function buildFunctionStorageProxy(
1826
1887
  const prefix = (key: string) => `${bucket}/${key}`;
1827
1888
 
1828
1889
  return {
1890
+ bucket(name) {
1891
+ return buildFunctionStorageProxy(r2, name, env, workerUrl);
1892
+ },
1893
+
1829
1894
  async put(key, value, options) {
1830
1895
  const httpMeta: R2PutOptions = {};
1831
1896
  if (options?.contentType) httpMeta.httpMetadata = { contentType: options.contentType };
@@ -1853,11 +1918,36 @@ export function buildFunctionStorageProxy(
1853
1918
  if (!secret) throw new Error('Signed URLs require JWT_USER_SECRET to be configured.');
1854
1919
  const expiresIn = options?.expiresIn ?? 3600;
1855
1920
  const expiresAt = Date.now() + expiresIn * 1000;
1856
- const token = await createSignedToken(key, bucket, expiresAt, secret);
1921
+ const obj = await r2.head(prefix(key));
1922
+ const token = await createSignedToken(key, bucket, expiresAt, secret, obj ? {
1923
+ file: {
1924
+ size: obj.size,
1925
+ contentType: obj.httpMetadata?.contentType || 'application/octet-stream',
1926
+ etag: obj.etag,
1927
+ uploadedAt: obj.uploaded?.toISOString(),
1928
+ },
1929
+ } : undefined);
1857
1930
  const base = workerUrl ?? 'http://localhost:8787';
1858
1931
  return `${base}/api/storage/${encodeURIComponent(bucket)}/${key}?token=${token}`;
1859
1932
  },
1860
1933
 
1934
+ async getSignedUploadUrl(key, options) {
1935
+ const secret = (env as unknown as Record<string, string>).JWT_USER_SECRET;
1936
+ if (!secret) throw new Error('Signed upload URLs require JWT_USER_SECRET to be configured.');
1937
+ const expiresIn = options?.expiresIn ?? 1800;
1938
+ const expiresAt = Date.now() + expiresIn * 1000;
1939
+ const maxBytes = typeof options?.maxBytes === 'number' && Number.isFinite(options.maxBytes)
1940
+ ? Math.max(0, Math.trunc(options.maxBytes))
1941
+ : null;
1942
+ const token = await createSignedToken(key, bucket, expiresAt, secret, maxBytes);
1943
+ const base = workerUrl ?? 'http://localhost:8787';
1944
+ return {
1945
+ url: `${base}/api/storage/${encodeURIComponent(bucket)}/upload?token=${token}&key=${encodeURIComponent(key)}`,
1946
+ expiresAt: new Date(expiresAt).toISOString(),
1947
+ maxBytes,
1948
+ };
1949
+ },
1950
+
1861
1951
  async list(options) {
1862
1952
  const r2Options: R2ListOptions = {};
1863
1953
  if (options?.prefix) r2Options.prefix = prefix(options.prefix);
@@ -11,6 +11,7 @@ import type { EdgeBaseConfig } from '@edge-base/shared';
11
11
  import {
12
12
  callDO,
13
13
  formatDbTargetValidationIssue,
14
+ getD1BindingName,
14
15
  getDbDoName,
15
16
  resolveDbTarget,
16
17
  shouldRouteToD1,
@@ -94,6 +95,12 @@ function parsePath(
94
95
  return { namespace, instanceId, tableName, directPath };
95
96
  }
96
97
 
98
+ function hasD1Binding(env: Env | undefined, namespace: string): boolean {
99
+ if (!env) return false;
100
+ const bindingName = getD1BindingName(namespace);
101
+ return Boolean((env as unknown as Record<string, unknown>)[bindingName]);
102
+ }
103
+
97
104
  export class InternalHttpTransport implements HttpTransport {
98
105
  private readonly databaseNamespace: DurableObjectNamespace;
99
106
  private readonly config: EdgeBaseConfig;
@@ -196,7 +203,14 @@ export class InternalHttpTransport implements HttpTransport {
196
203
  const httpMethod = method === 'PUT' ? 'PATCH' : method; // normalize PUT → PATCH
197
204
 
198
205
  // 1. D1 route
199
- if (!this.preferDirectDo && !dynamic && shouldRouteToD1(namespace, this.config) && this.env) {
206
+ const shouldUseD1 =
207
+ !this.preferDirectDo
208
+ && !dynamic
209
+ && shouldRouteToD1(namespace, this.config)
210
+ && this.env
211
+ && (provider === 'd1' || hasD1Binding(this.env, namespace));
212
+
213
+ if (shouldUseD1) {
200
214
  return this.requestViaD1Handler(httpMethod, namespace, normalizedInstanceId, tableName, directPath, headers, query, body);
201
215
  }
202
216
 
package/src/lib/jwt.ts CHANGED
@@ -22,6 +22,7 @@ export interface RefreshTokenPayload {
22
22
  sub: string; // userId
23
23
  type: 'refresh';
24
24
  jti?: string; // unique session ID for token uniqueness
25
+ nonce?: string; // per-token randomness while preserving session jti
25
26
  }
26
27
 
27
28
  export interface AdminTokenPayload {
@@ -106,6 +107,7 @@ export async function signRefreshToken(
106
107
  ): Promise<string> {
107
108
  const builder = new SignJWT({
108
109
  type: 'refresh',
110
+ nonce: payload.nonce ?? crypto.randomUUID(),
109
111
  })
110
112
  .setProtectedHeader({ alg: ALGORITHM })
111
113
  .setIssuedAt()
@@ -1539,7 +1539,14 @@ api.openapi(adminCreateSignedUrl, async (c) => {
1539
1539
 
1540
1540
  const expiresInMs = parseDuration(body.expiresIn || '1h');
1541
1541
  const expiresAt = Date.now() + expiresInMs;
1542
- const token = await createSignedToken(body.key, bucketName, expiresAt, secret);
1542
+ const token = await createSignedToken(body.key, bucketName, expiresAt, secret, {
1543
+ file: {
1544
+ size: obj.size,
1545
+ contentType: obj.httpMetadata?.contentType || 'application/octet-stream',
1546
+ etag: obj.etag,
1547
+ uploadedAt: obj.uploaded?.toISOString(),
1548
+ },
1549
+ });
1543
1550
 
1544
1551
  // Build signed URL using the public storage endpoint
1545
1552
  const url = new URL(c.req.url);