@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (176) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index-DXVBFp5V.js +37 -0
  6. package/dist/index-DXVBFp5V.js.map +1 -0
  7. package/dist/index.es.js +49931 -55
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +28475 -34569
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
  12. package/dist/server-core/src/api/types.d.ts +1 -1
  13. package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
  14. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  15. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  16. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  17. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  18. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  19. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  20. package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
  21. package/dist/server-core/src/auth/index.d.ts +13 -2
  22. package/dist/server-core/src/auth/interfaces.d.ts +45 -6
  23. package/dist/server-core/src/auth/jwt.d.ts +1 -0
  24. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  25. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  26. package/dist/server-core/src/auth/middleware.d.ts +36 -11
  27. package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
  28. package/dist/server-core/src/auth/routes.d.ts +13 -3
  29. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  30. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  31. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  32. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  33. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  34. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  35. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  36. package/dist/server-core/src/cron/index.d.ts +6 -0
  37. package/dist/server-core/src/email/index.d.ts +2 -2
  38. package/dist/server-core/src/email/templates.d.ts +9 -0
  39. package/dist/server-core/src/email/types.d.ts +20 -23
  40. package/dist/server-core/src/history/history-routes.d.ts +2 -2
  41. package/dist/server-core/src/index.d.ts +5 -0
  42. package/dist/server-core/src/init.d.ts +112 -2
  43. package/dist/server-core/src/singleton.d.ts +35 -0
  44. package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
  45. package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
  46. package/dist/server-core/src/storage/index.d.ts +18 -11
  47. package/dist/server-core/src/storage/routes.d.ts +3 -3
  48. package/dist/server-core/src/storage/types.d.ts +28 -16
  49. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  50. package/dist/server-core/src/utils/logger.d.ts +31 -0
  51. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  52. package/dist/types/src/controllers/auth.d.ts +2 -0
  53. package/dist/types/src/controllers/client.d.ts +119 -7
  54. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  55. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  56. package/dist/types/src/controllers/data.d.ts +34 -7
  57. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  58. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  59. package/dist/types/src/controllers/email.d.ts +34 -0
  60. package/dist/types/src/controllers/index.d.ts +1 -0
  61. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  62. package/dist/types/src/controllers/navigation.d.ts +5 -5
  63. package/dist/types/src/controllers/registry.d.ts +6 -3
  64. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  65. package/dist/types/src/controllers/storage.d.ts +24 -26
  66. package/dist/types/src/rebase_context.d.ts +8 -4
  67. package/dist/types/src/types/backend.d.ts +4 -1
  68. package/dist/types/src/types/builders.d.ts +5 -4
  69. package/dist/types/src/types/chips.d.ts +1 -1
  70. package/dist/types/src/types/collections.d.ts +169 -125
  71. package/dist/types/src/types/cron.d.ts +102 -0
  72. package/dist/types/src/types/data_source.d.ts +1 -1
  73. package/dist/types/src/types/entity_actions.d.ts +8 -8
  74. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  75. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  76. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  77. package/dist/types/src/types/entity_views.d.ts +8 -8
  78. package/dist/types/src/types/export_import.d.ts +3 -3
  79. package/dist/types/src/types/index.d.ts +1 -0
  80. package/dist/types/src/types/plugins.d.ts +72 -18
  81. package/dist/types/src/types/properties.d.ts +118 -33
  82. package/dist/types/src/types/relations.d.ts +1 -1
  83. package/dist/types/src/types/slots.d.ts +30 -6
  84. package/dist/types/src/types/translations.d.ts +44 -0
  85. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  86. package/package.json +84 -84
  87. package/scratch.ts +6 -5
  88. package/src/api/ast-schema-editor.ts +11 -11
  89. package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
  90. package/src/api/errors.ts +38 -14
  91. package/src/api/graphql/graphql-schema-generator.ts +5 -3
  92. package/src/api/openapi-generator.ts +613 -58
  93. package/src/api/rest/api-generator.ts +17 -11
  94. package/src/api/rest/query-parser.ts +1 -1
  95. package/src/api/schema-editor-routes.ts +2 -0
  96. package/src/api/server.ts +8 -5
  97. package/src/api/types.ts +1 -1
  98. package/src/auth/admin-routes.ts +58 -17
  99. package/src/auth/apple-oauth.ts +130 -0
  100. package/src/auth/bitbucket-oauth.ts +82 -0
  101. package/src/auth/discord-oauth.ts +83 -0
  102. package/src/auth/facebook-oauth.ts +72 -0
  103. package/src/auth/github-oauth.ts +110 -0
  104. package/src/auth/gitlab-oauth.ts +70 -0
  105. package/src/auth/google-oauth.ts +34 -46
  106. package/src/auth/index.ts +15 -2
  107. package/src/auth/interfaces.ts +54 -7
  108. package/src/auth/jwt.ts +22 -5
  109. package/src/auth/linkedin-oauth.ts +81 -0
  110. package/src/auth/microsoft-oauth.ts +88 -0
  111. package/src/auth/middleware.ts +194 -45
  112. package/src/auth/password.ts +3 -1
  113. package/src/auth/rate-limiter.ts +4 -4
  114. package/src/auth/routes.ts +197 -139
  115. package/src/auth/slack-oauth.ts +71 -0
  116. package/src/auth/spotify-oauth.ts +67 -0
  117. package/src/auth/twitter-oauth.ts +120 -0
  118. package/src/collections/loader.ts +6 -6
  119. package/src/cron/cron-loader.ts +89 -0
  120. package/src/cron/cron-routes.test.ts +265 -0
  121. package/src/cron/cron-routes.ts +85 -0
  122. package/src/cron/cron-scheduler.test.ts +421 -0
  123. package/src/cron/cron-scheduler.ts +413 -0
  124. package/src/cron/cron-store.ts +163 -0
  125. package/src/cron/index.ts +6 -0
  126. package/src/db/interfaces.ts +2 -2
  127. package/src/email/index.ts +3 -2
  128. package/src/email/smtp-email-service.ts +5 -2
  129. package/src/email/templates.ts +93 -6
  130. package/src/email/types.ts +17 -24
  131. package/src/functions/function-loader.ts +34 -6
  132. package/src/functions/function-routes.ts +2 -2
  133. package/src/history/history-routes.ts +4 -3
  134. package/src/index.ts +10 -0
  135. package/src/init.ts +475 -57
  136. package/src/services/driver-registry.ts +1 -1
  137. package/src/singleton.test.ts +28 -0
  138. package/src/singleton.ts +70 -0
  139. package/src/storage/LocalStorageController.ts +50 -53
  140. package/src/storage/S3StorageController.ts +49 -46
  141. package/src/storage/index.ts +27 -16
  142. package/src/storage/routes.ts +76 -59
  143. package/src/storage/storage-registry.ts +5 -5
  144. package/src/storage/types.ts +44 -32
  145. package/src/types/index.ts +4 -4
  146. package/src/utils/dev-port.ts +176 -0
  147. package/src/utils/logger.ts +143 -0
  148. package/src/utils/logging.ts +5 -2
  149. package/src/utils/request-logger.ts +66 -0
  150. package/test/admin-routes.test.ts +114 -65
  151. package/test/api-generator.test.ts +73 -30
  152. package/test/ast-schema-editor.test.ts +5 -3
  153. package/test/auth-middleware-hono.test.ts +268 -33
  154. package/test/auth-routes.test.ts +325 -146
  155. package/test/driver-registry.test.ts +4 -2
  156. package/test/error-propagation.test.ts +226 -0
  157. package/test/errors.test.ts +12 -7
  158. package/test/jwt-security.test.ts +18 -9
  159. package/test/jwt.test.ts +26 -13
  160. package/test/middleware.test.ts +13 -8
  161. package/test/query-parser.test.ts +13 -8
  162. package/test/rate-limiter.test.ts +7 -7
  163. package/test/safe-compare.test.ts +66 -0
  164. package/test/singleton.test.ts +59 -0
  165. package/test/storage-local.test.ts +36 -43
  166. package/test/storage-registry.test.ts +38 -36
  167. package/test/storage-routes.test.ts +9 -5
  168. package/test/storage-s3.test.ts +51 -48
  169. package/test.ts +6 -0
  170. package/tsconfig.json +1 -1
  171. package/vite.config.ts +7 -5
  172. package/dist/index-BeMqpmfQ.js +0 -239
  173. package/dist/index-BeMqpmfQ.js.map +0 -1
  174. package/dist/index-bl4J3lNb.js +0 -55823
  175. package/dist/index-bl4J3lNb.js.map +0 -1
  176. package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
@@ -9,7 +9,7 @@ describe("Rate Limiter", () => {
9
9
  const limiter = createRateLimiter({
10
10
  windowMs: options.windowMs ?? 60 * 1000, // 1 minute
11
11
  limit: options.limit ?? 3,
12
- keyGenerator: (c) => c.req.header("x-forwarded-for") || "test-ip",
12
+ keyGenerator: (c) => c.req.header("x-forwarded-for") || "test-ip"
13
13
  });
14
14
  app.use("/api/*", limiter);
15
15
  app.get("/api/test", (c) => c.json({ ok: true }));
@@ -20,7 +20,7 @@ describe("Rate Limiter", () => {
20
20
  const app = createTestApp({ limit: 5 });
21
21
 
22
22
  const res = await app.request("/api/test", {
23
- headers: { "x-forwarded-for": "1.2.3.4" },
23
+ headers: { "x-forwarded-for": "1.2.3.4" }
24
24
  });
25
25
 
26
26
  expect(res.status).toBe(200);
@@ -37,7 +37,7 @@ describe("Rate Limiter", () => {
37
37
 
38
38
  // Third should be rate limited
39
39
  const res = await app.request("/api/test", {
40
- headers: { "x-forwarded-for": "10.0.0.1" },
40
+ headers: { "x-forwarded-for": "10.0.0.1" }
41
41
  });
42
42
 
43
43
  expect(res.status).toBe(429);
@@ -50,7 +50,7 @@ describe("Rate Limiter", () => {
50
50
 
51
51
  await app.request("/api/test", { headers: { "x-forwarded-for": "10.0.0.2" } });
52
52
  const res = await app.request("/api/test", {
53
- headers: { "x-forwarded-for": "10.0.0.2" },
53
+ headers: { "x-forwarded-for": "10.0.0.2" }
54
54
  });
55
55
 
56
56
  expect(res.headers.get("Retry-After")).toBeDefined();
@@ -61,10 +61,10 @@ describe("Rate Limiter", () => {
61
61
  const app = createTestApp({ limit: 1 });
62
62
 
63
63
  const res1 = await app.request("/api/test", {
64
- headers: { "x-forwarded-for": "ip-a" },
64
+ headers: { "x-forwarded-for": "ip-a" }
65
65
  });
66
66
  const res2 = await app.request("/api/test", {
67
- headers: { "x-forwarded-for": "ip-b" },
67
+ headers: { "x-forwarded-for": "ip-b" }
68
68
  });
69
69
 
70
70
  expect(res1.status).toBe(200);
@@ -90,7 +90,7 @@ describe("Rate Limiter", () => {
90
90
  const limiter = createRateLimiter({
91
91
  limit: 0,
92
92
  message: "Slow down!",
93
- keyGenerator: () => "always-same",
93
+ keyGenerator: () => "always-same"
94
94
  });
95
95
  app.use("/api/*", limiter);
96
96
  app.get("/api/test", (c) => c.json({ ok: true }));
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tests for the safeCompare function used in auth middleware.
3
+ *
4
+ * Since safeCompare is not exported, we test it indirectly through
5
+ * the createAuthMiddleware behavior with service keys. However, for
6
+ * unit-level verification we replicate the logic here.
7
+ */
8
+ import { timingSafeEqual } from "crypto";
9
+
10
+ // Replicate the safeCompare implementation to test in isolation
11
+ function safeCompare(a: string, b: string): boolean {
12
+ const maxLen = Math.max(a.length, b.length);
13
+ const bufA = Buffer.alloc(maxLen);
14
+ const bufB = Buffer.alloc(maxLen);
15
+ bufA.write(a);
16
+ bufB.write(b);
17
+ try {
18
+ const isEqual = timingSafeEqual(bufA, bufB);
19
+ return isEqual && a.length === b.length;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ describe("safeCompare", () => {
26
+ it("should return true for identical strings", () => {
27
+ expect(safeCompare("my-secret-key-12345678901234567890", "my-secret-key-12345678901234567890")).toBe(true);
28
+ });
29
+
30
+ it("should return false for different strings of same length", () => {
31
+ expect(safeCompare("aaaa", "bbbb")).toBe(false);
32
+ });
33
+
34
+ it("should return false for different length strings", () => {
35
+ expect(safeCompare("short", "a-much-longer-string")).toBe(false);
36
+ });
37
+
38
+ it("should return false when one string is a prefix of the other", () => {
39
+ // This is the critical case: "abc" vs "abc\0\0\0" would match
40
+ // without the length check, since Buffer.alloc zero-fills
41
+ expect(safeCompare("abc", "abc\0\0\0")).toBe(false);
42
+ });
43
+
44
+ it("should return false for empty string vs non-empty", () => {
45
+ expect(safeCompare("", "something")).toBe(false);
46
+ });
47
+
48
+ it("should return true for two empty strings", () => {
49
+ expect(safeCompare("", "")).toBe(true);
50
+ });
51
+
52
+ it("should handle unicode strings", () => {
53
+ expect(safeCompare("café", "café")).toBe(true);
54
+ expect(safeCompare("café", "cafe")).toBe(false);
55
+ });
56
+
57
+ it("should not leak length information via early return", () => {
58
+ // Both comparisons should take similar time (constant-time)
59
+ // We can't easily assert timing, but we verify they both
60
+ // go through the same code path
61
+ const result1 = safeCompare("a", "bb");
62
+ const result2 = safeCompare("aa", "bb");
63
+ expect(result1).toBe(false);
64
+ expect(result2).toBe(false);
65
+ });
66
+ });
@@ -0,0 +1,59 @@
1
+ import { rebase, _initRebase, _setRebaseMock, _resetRebaseMock } from "../src/singleton";
2
+ import type { RebaseClient } from "@rebasepro/types";
3
+
4
+ describe("rebase singleton", () => {
5
+ afterEach(() => {
6
+ // Reset after each test to avoid interference
7
+ process.env.NODE_ENV = "test";
8
+ _resetRebaseMock();
9
+ });
10
+
11
+ it("should throw when accessing properties before initialization", () => {
12
+ expect(() => rebase.data).toThrowError(/server not initialized yet/);
13
+ });
14
+
15
+ it("should allow property access after initialization", () => {
16
+ const mockData = { find: jest.fn() };
17
+ _setRebaseMock({ data: mockData } as Partial<RebaseClient>);
18
+
19
+ expect(rebase.data).toBe(mockData);
20
+ });
21
+
22
+ it("should throw on property assignment (set trap)", () => {
23
+ _setRebaseMock({ data: {} } as Partial<RebaseClient>);
24
+
25
+ expect(() => {
26
+ (rebase as Record<string, unknown>).data = "overwritten";
27
+ }).toThrowError(/Cannot set rebase\.data directly/);
28
+ });
29
+
30
+ it("should reset properly with _resetRebaseMock", () => {
31
+ _setRebaseMock({ data: {} } as Partial<RebaseClient>);
32
+ expect(() => rebase.data).not.toThrow();
33
+
34
+ _resetRebaseMock();
35
+ expect(() => rebase.data).toThrowError(/server not initialized yet/);
36
+ });
37
+
38
+ it("should throw if _setRebaseMock is called outside test env", () => {
39
+ const originalEnv = process.env.NODE_ENV;
40
+ process.env.NODE_ENV = "production";
41
+
42
+ expect(() => _setRebaseMock({} as Partial<RebaseClient>)).toThrowError(
43
+ /can only be called in a test environment/
44
+ );
45
+
46
+ process.env.NODE_ENV = originalEnv;
47
+ });
48
+
49
+ it("should throw if _resetRebaseMock is called outside test env", () => {
50
+ const originalEnv = process.env.NODE_ENV;
51
+ process.env.NODE_ENV = "production";
52
+
53
+ expect(() => _resetRebaseMock()).toThrowError(
54
+ /can only be called in a test environment/
55
+ );
56
+
57
+ process.env.NODE_ENV = originalEnv;
58
+ });
59
+ });
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
1
2
  import * as fs from "fs";
2
3
  import * as path from "path";
3
4
  import * as os from "os";
@@ -17,7 +18,8 @@ describe("LocalStorageController", () => {
17
18
 
18
19
  afterEach(async () => {
19
20
  // Clean up temporary directory
20
- await fs.promises.rm(tempDir, { recursive: true, force: true });
21
+ await fs.promises.rm(tempDir, { recursive: true,
22
+ force: true });
21
23
  });
22
24
 
23
25
  describe("constructor", () => {
@@ -30,19 +32,18 @@ describe("LocalStorageController", () => {
30
32
  });
31
33
  });
32
34
 
33
- describe("uploadFile", () => {
35
+ describe("putObject", () => {
34
36
  it("should upload a file and return metadata", async () => {
35
37
  const content = Buffer.from("Hello, World!");
36
38
  const file = new File([content], "test.txt", { type: "text/plain" });
37
39
 
38
- const result = await controller.uploadFile({
40
+ const result = await controller.putObject({
39
41
  file,
40
- fileName: "test.txt",
41
- path: "uploads"
42
+ key: "uploads/test.txt"
42
43
  });
43
44
 
44
- expect(result.path).toContain("uploads");
45
- expect(result.path).toContain("test.txt");
45
+ expect(result.key).toContain("uploads");
46
+ expect(result.key).toContain("test.txt");
46
47
 
47
48
  // Verify file exists on disk (uses default bucket)
48
49
  const filePath = path.join(tempDir, "default", "uploads", "test.txt");
@@ -54,10 +55,9 @@ describe("LocalStorageController", () => {
54
55
  const content = Buffer.from("Nested content");
55
56
  const file = new File([content], "nested.txt", { type: "text/plain" });
56
57
 
57
- await controller.uploadFile({
58
+ await controller.putObject({
58
59
  file,
59
- fileName: "nested.txt",
60
- path: "level1/level2/level3"
60
+ key: "level1/level2/level3/nested.txt"
61
61
  });
62
62
 
63
63
  const filePath = path.join(tempDir, "default", "level1", "level2", "level3", "nested.txt");
@@ -69,10 +69,9 @@ describe("LocalStorageController", () => {
69
69
  const content = Buffer.from("Bucket content");
70
70
  const file = new File([content], "bucket.txt", { type: "text/plain" });
71
71
 
72
- await controller.uploadFile({
72
+ await controller.putObject({
73
73
  file,
74
- fileName: "bucket.txt",
75
- path: "files",
74
+ key: "files/bucket.txt",
76
75
  bucket: "custom-bucket"
77
76
  });
78
77
 
@@ -85,10 +84,9 @@ describe("LocalStorageController", () => {
85
84
  const content = Buffer.from("With metadata");
86
85
  const file = new File([content], "meta.txt", { type: "text/plain" });
87
86
 
88
- await controller.uploadFile({
87
+ await controller.putObject({
89
88
  file,
90
- fileName: "meta.txt",
91
- path: "uploads",
89
+ key: "uploads/meta.txt",
92
90
  metadata: { customField: "customValue" }
93
91
  });
94
92
 
@@ -99,39 +97,37 @@ describe("LocalStorageController", () => {
99
97
  });
100
98
  });
101
99
 
102
- describe("getFile", () => {
100
+ describe("getObject", () => {
103
101
  it("should retrieve an uploaded file using local:// URL format", async () => {
104
102
  const content = Buffer.from("Retrieve me");
105
103
  const file = new File([content], "retrieve.txt", { type: "text/plain" });
106
104
 
107
- const uploadResult = await controller.uploadFile({
105
+ const uploadResult = await controller.putObject({
108
106
  file,
109
- fileName: "retrieve.txt",
110
- path: "uploads"
107
+ key: "uploads/retrieve.txt"
111
108
  });
112
109
 
113
110
  // Use the storageUrl from upload result (local:// format)
114
- const retrieved = await controller.getFile(uploadResult.storageUrl!);
111
+ const retrieved = await controller.getObject(uploadResult.storageUrl!);
115
112
 
116
113
  expect(retrieved).not.toBeNull();
117
114
  expect(retrieved?.name).toBe("retrieve.txt");
118
115
  });
119
116
 
120
117
  it("should return null for non-existent file", async () => {
121
- const result = await controller.getFile("local://default/nonexistent/file.txt");
118
+ const result = await controller.getObject("local://default/nonexistent/file.txt");
122
119
  expect(result).toBeNull();
123
120
  });
124
121
  });
125
122
 
126
- describe("deleteFile", () => {
123
+ describe("deleteObject", () => {
127
124
  it("should delete an uploaded file using local:// URL format", async () => {
128
125
  const content = Buffer.from("Delete me");
129
126
  const file = new File([content], "delete.txt", { type: "text/plain" });
130
127
 
131
- const uploadResult = await controller.uploadFile({
128
+ const uploadResult = await controller.putObject({
132
129
  file,
133
- fileName: "delete.txt",
134
- path: "uploads"
130
+ key: "uploads/delete.txt"
135
131
  });
136
132
 
137
133
  // Verify file exists
@@ -140,7 +136,7 @@ describe("LocalStorageController", () => {
140
136
  expect(exists).toBe(true);
141
137
 
142
138
  // Delete the file using local:// URL format
143
- await controller.deleteFile(uploadResult.storageUrl!);
139
+ await controller.deleteObject(uploadResult.storageUrl!);
144
140
 
145
141
  // Verify file no longer exists
146
142
  exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
@@ -148,21 +144,20 @@ describe("LocalStorageController", () => {
148
144
  });
149
145
 
150
146
  it("should not throw when deleting non-existent file", async () => {
151
- await expect(controller.deleteFile("local://default/nonexistent/file.txt")).resolves.not.toThrow();
147
+ await expect(controller.deleteObject("local://default/nonexistent/file.txt")).resolves.not.toThrow();
152
148
  });
153
149
 
154
150
  it("should also delete metadata file", async () => {
155
151
  const content = Buffer.from("Delete with metadata");
156
152
  const file = new File([content], "withmeta.txt", { type: "text/plain" });
157
153
 
158
- const uploadResult = await controller.uploadFile({
154
+ const uploadResult = await controller.putObject({
159
155
  file,
160
- fileName: "withmeta.txt",
161
- path: "uploads",
156
+ key: "uploads/withmeta.txt",
162
157
  metadata: { key: "value" }
163
158
  });
164
159
 
165
- await controller.deleteFile(uploadResult.storageUrl!);
160
+ await controller.deleteObject(uploadResult.storageUrl!);
166
161
 
167
162
  const metadataPath = path.join(tempDir, "default", "uploads", "withmeta.txt.metadata.json");
168
163
  const exists = await fs.promises.access(metadataPath).then(() => true).catch(() => false);
@@ -175,48 +170,46 @@ describe("LocalStorageController", () => {
175
170
  // Upload some test files
176
171
  for (let i = 1; i <= 5; i++) {
177
172
  const file = new File([`Content ${i}`], `file${i}.txt`, { type: "text/plain" });
178
- await controller.uploadFile({
173
+ await controller.putObject({
179
174
  file,
180
- fileName: `file${i}.txt`,
181
- path: "listtest"
175
+ key: `listtest/file${i}.txt`
182
176
  });
183
177
  }
184
178
  });
185
179
 
186
180
  it("should list files in a directory", async () => {
187
- const result = await controller.list("listtest", { bucket: "default" });
181
+ const result = await controller.listObjects("listtest", { bucket: "default" });
188
182
 
189
183
  // Items should be the actual files (not metadata files)
190
184
  expect(result.items.length).toBeGreaterThanOrEqual(5);
191
185
  });
192
186
 
193
187
  it("should return empty list for non-existent directory", async () => {
194
- const result = await controller.list("nonexistent", { bucket: "default" });
188
+ const result = await controller.listObjects("nonexistent", { bucket: "default" });
195
189
 
196
190
  expect(result.items).toHaveLength(0);
197
191
  });
198
192
  });
199
193
 
200
- describe("getDownloadURL", () => {
194
+ describe("getSignedUrl", () => {
201
195
  it("should return download URL for existing file", async () => {
202
196
  const content = Buffer.from("Download me");
203
197
  const file = new File([content], "download.txt", { type: "text/plain" });
204
198
 
205
- const uploadResult = await controller.uploadFile({
199
+ const uploadResult = await controller.putObject({
206
200
  file,
207
- fileName: "download.txt",
208
- path: "uploads"
201
+ key: "uploads/download.txt"
209
202
  });
210
203
 
211
204
  // Use the storageUrl from upload result
212
- const result = await controller.getDownloadURL(uploadResult.storageUrl!);
205
+ const result = await controller.getSignedUrl(uploadResult.storageUrl!);
213
206
 
214
207
  expect(result.url).toBeTruthy();
215
208
  expect(result.fileNotFound).toBeFalsy();
216
209
  });
217
210
 
218
211
  it("should return fileNotFound for non-existent file", async () => {
219
- const result = await controller.getDownloadURL("local://default/nonexistent/file.txt");
212
+ const result = await controller.getSignedUrl("local://default/nonexistent/file.txt");
220
213
 
221
214
  expect(result.fileNotFound).toBe(true);
222
215
  });
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
1
2
  import {
2
3
  StorageRegistry,
3
4
  DefaultStorageRegistry,
@@ -8,14 +9,15 @@ import { StorageController } from "../src/storage/types";
8
9
  /**
9
10
  * Mock StorageController for testing
10
11
  */
11
- function createMockStorageController(type: 'local' | 's3'): StorageController {
12
+ function createMockStorageController(type: "local" | "s3"): StorageController {
12
13
  return {
13
- uploadFile: jest.fn().mockResolvedValue({ path: "test/file.txt" }),
14
- getDownloadURL: jest.fn().mockResolvedValue({ url: "http://example.com/file.txt" }),
15
- getFile: jest.fn().mockResolvedValue(null),
16
- deleteFile: jest.fn().mockResolvedValue(undefined),
17
- list: jest.fn().mockResolvedValue({ items: [], prefixes: [] }),
18
- getType: jest.fn().mockReturnValue(type)
14
+ putObject: vi.fn().mockResolvedValue({ path: "test/file.txt" }),
15
+ getSignedUrl: vi.fn().mockResolvedValue({ url: "http://example.com/file.txt" }),
16
+ getObject: vi.fn().mockResolvedValue(null),
17
+ deleteObject: vi.fn().mockResolvedValue(undefined),
18
+ listObjects: vi.fn().mockResolvedValue({ items: [],
19
+ prefixes: [] }),
20
+ getType: vi.fn().mockReturnValue(type)
19
21
  };
20
22
  }
21
23
 
@@ -36,7 +38,7 @@ describe("StorageRegistry", () => {
36
38
 
37
39
  it("should register a storage controller", () => {
38
40
  const registry = new DefaultStorageRegistry();
39
- const mockController = createMockStorageController('local');
41
+ const mockController = createMockStorageController("local");
40
42
 
41
43
  registry.register("test-storage", mockController);
42
44
 
@@ -47,7 +49,7 @@ describe("StorageRegistry", () => {
47
49
 
48
50
  it("should get a registered storage controller", () => {
49
51
  const registry = new DefaultStorageRegistry();
50
- const mockController = createMockStorageController('s3');
52
+ const mockController = createMockStorageController("s3");
51
53
 
52
54
  registry.register("my-storage", mockController);
53
55
 
@@ -64,7 +66,7 @@ describe("StorageRegistry", () => {
64
66
  describe("default storage handling", () => {
65
67
  it("should get default storage with get(undefined)", () => {
66
68
  const registry = new DefaultStorageRegistry();
67
- const mockController = createMockStorageController('local');
69
+ const mockController = createMockStorageController("local");
68
70
 
69
71
  registry.register(DEFAULT_STORAGE_ID, mockController);
70
72
 
@@ -74,7 +76,7 @@ describe("StorageRegistry", () => {
74
76
 
75
77
  it("should get default storage with getDefault()", () => {
76
78
  const registry = new DefaultStorageRegistry();
77
- const mockController = createMockStorageController('local');
79
+ const mockController = createMockStorageController("local");
78
80
 
79
81
  registry.register(DEFAULT_STORAGE_ID, mockController);
80
82
 
@@ -85,7 +87,7 @@ describe("StorageRegistry", () => {
85
87
  const registry = new DefaultStorageRegistry();
86
88
 
87
89
  expect(() => registry.getDefault()).toThrow(
88
- `[StorageRegistry] No default storage registered.`
90
+ "[StorageRegistry] No default storage registered."
89
91
  );
90
92
  });
91
93
  });
@@ -97,8 +99,8 @@ describe("StorageRegistry", () => {
97
99
 
98
100
  beforeEach(() => {
99
101
  registry = new DefaultStorageRegistry();
100
- defaultController = createMockStorageController('local');
101
- mediaController = createMockStorageController('s3');
102
+ defaultController = createMockStorageController("local");
103
+ mediaController = createMockStorageController("s3");
102
104
 
103
105
  registry.register(DEFAULT_STORAGE_ID, defaultController);
104
106
  registry.register("media", mediaController);
@@ -117,7 +119,7 @@ describe("StorageRegistry", () => {
117
119
  });
118
120
 
119
121
  it("should fallback to default when id not found", () => {
120
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
122
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
121
123
 
122
124
  expect(registry.getOrDefault("non-existent")).toBe(defaultController);
123
125
  expect(consoleSpy).toHaveBeenCalledWith(
@@ -137,10 +139,10 @@ describe("StorageRegistry", () => {
137
139
  describe("overwriting storages", () => {
138
140
  it("should overwrite existing storage with same id", () => {
139
141
  const registry = new DefaultStorageRegistry();
140
- const original = createMockStorageController('local');
141
- const replacement = createMockStorageController('s3');
142
+ const original = createMockStorageController("local");
143
+ const replacement = createMockStorageController("s3");
142
144
 
143
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
145
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
144
146
 
145
147
  registry.register("my-storage", original);
146
148
  registry.register("my-storage", replacement);
@@ -158,9 +160,9 @@ describe("StorageRegistry", () => {
158
160
  it("should list all registered storages", () => {
159
161
  const registry = new DefaultStorageRegistry();
160
162
 
161
- registry.register("storage-1", createMockStorageController('local'));
162
- registry.register("storage-2", createMockStorageController('s3'));
163
- registry.register(DEFAULT_STORAGE_ID, createMockStorageController('local'));
163
+ registry.register("storage-1", createMockStorageController("local"));
164
+ registry.register("storage-2", createMockStorageController("s3"));
165
+ registry.register(DEFAULT_STORAGE_ID, createMockStorageController("local"));
164
166
 
165
167
  const list = registry.list();
166
168
  expect(list).toHaveLength(3);
@@ -174,10 +176,10 @@ describe("StorageRegistry", () => {
174
176
 
175
177
  expect(registry.size()).toBe(0);
176
178
 
177
- registry.register("storage-1", createMockStorageController('local'));
179
+ registry.register("storage-1", createMockStorageController("local"));
178
180
  expect(registry.size()).toBe(1);
179
181
 
180
- registry.register("storage-2", createMockStorageController('s3'));
182
+ registry.register("storage-2", createMockStorageController("s3"));
181
183
  expect(registry.size()).toBe(2);
182
184
  });
183
185
  });
@@ -186,7 +188,7 @@ describe("StorageRegistry", () => {
186
188
  describe("DefaultStorageRegistry.create() factory", () => {
187
189
  describe("with single StorageController", () => {
188
190
  it('should register single controller as "(default)"', () => {
189
- const mockController = createMockStorageController('local');
191
+ const mockController = createMockStorageController("local");
190
192
 
191
193
  const registry = DefaultStorageRegistry.create(mockController);
192
194
 
@@ -198,8 +200,8 @@ describe("StorageRegistry", () => {
198
200
 
199
201
  describe("with map of StorageControllers", () => {
200
202
  it("should register all controllers from map", () => {
201
- const defaultController = createMockStorageController('local');
202
- const mediaController = createMockStorageController('s3');
203
+ const defaultController = createMockStorageController("local");
204
+ const mediaController = createMockStorageController("s3");
203
205
 
204
206
  const registry = DefaultStorageRegistry.create({
205
207
  [DEFAULT_STORAGE_ID]: defaultController,
@@ -212,10 +214,10 @@ describe("StorageRegistry", () => {
212
214
  });
213
215
 
214
216
  it("should use first entry as default if no explicit default provided", () => {
215
- const local = createMockStorageController('local');
216
- const s3 = createMockStorageController('s3');
217
+ const local = createMockStorageController("local");
218
+ const s3 = createMockStorageController("s3");
217
219
 
218
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
220
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation();
219
221
 
220
222
  const registry = DefaultStorageRegistry.create({
221
223
  "primary": local,
@@ -243,7 +245,7 @@ describe("StorageRegistry", () => {
243
245
 
244
246
  describe("type detection (isStorageController)", () => {
245
247
  it("should correctly identify a StorageController", () => {
246
- const mockController = createMockStorageController('local');
248
+ const mockController = createMockStorageController("local");
247
249
 
248
250
  // The factory should recognize it as a single controller
249
251
  const registry = DefaultStorageRegistry.create(mockController);
@@ -253,8 +255,8 @@ describe("StorageRegistry", () => {
253
255
 
254
256
  it("should correctly identify a map of StorageControllers", () => {
255
257
  const controllers = {
256
- [DEFAULT_STORAGE_ID]: createMockStorageController('local'),
257
- "other": createMockStorageController('s3')
258
+ [DEFAULT_STORAGE_ID]: createMockStorageController("local"),
259
+ "other": createMockStorageController("s3")
258
260
  };
259
261
 
260
262
  // The factory should recognize it as a map
@@ -265,16 +267,16 @@ describe("StorageRegistry", () => {
265
267
 
266
268
  describe("integration with storage types", () => {
267
269
  it("should correctly report storage types", () => {
268
- const localController = createMockStorageController('local');
269
- const s3Controller = createMockStorageController('s3');
270
+ const localController = createMockStorageController("local");
271
+ const s3Controller = createMockStorageController("s3");
270
272
 
271
273
  const registry = DefaultStorageRegistry.create({
272
274
  [DEFAULT_STORAGE_ID]: localController,
273
275
  "media": s3Controller
274
276
  });
275
277
 
276
- expect(registry.getDefault().getType()).toBe('local');
277
- expect(registry.get("media")?.getType()).toBe('s3');
278
+ expect(registry.getDefault().getType()).toBe("local");
279
+ expect(registry.get("media")?.getType()).toBe("s3");
278
280
  });
279
281
  });
280
282
  });
@@ -1,3 +1,4 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest as vi } from "@jest/globals";
1
2
  /**
2
3
  * Tests for storage routes — specifically the sub-router wildcard extraction
3
4
  * that broke when Hono's c.req.param('*') stopped working in mounted sub-routers.
@@ -96,21 +97,23 @@ describe("Storage routes (sub-router integration)", () => {
96
97
  // Upload a test file so we have something to serve
97
98
  const content = Buffer.from("Hello test file");
98
99
  const file = new File([content], "test.txt", { type: "text/plain" });
99
- await controller.uploadFile({ file, fileName: "test.txt", path: "photos" });
100
+ await controller.putObject({ file,
101
+ key: "photos/test.txt" });
100
102
 
101
103
  // Create the Hono app with storage routes mounted as a SUB-ROUTER
102
104
  // (this is the exact pattern that caused the bug)
103
105
  app = new Hono<HonoEnv>();
104
- app.onError(errorHandler); // required to convert ApiError throws to proper HTTP responses
106
+ app.onError(errorHandler); // required to convert ApiError throws to proper HTTP responses
105
107
  const storageRoutes = createStorageRoutes({
106
108
  controller,
107
- requireAuth: false // skip auth for tests
109
+ requireAuth: false // skip auth for tests
108
110
  });
109
111
  app.route("/api/storage", storageRoutes);
110
112
  });
111
113
 
112
114
  afterEach(async () => {
113
- await fs.promises.rm(tempDir, { recursive: true, force: true });
115
+ await fs.promises.rm(tempDir, { recursive: true,
116
+ force: true });
114
117
  });
115
118
 
116
119
  describe("GET /metadata/*", () => {
@@ -192,7 +195,8 @@ describe("Storage routes (sub-router integration)", () => {
192
195
  it("should delete an existing file", async () => {
193
196
  // Upload another file to delete
194
197
  const file = new File([Buffer.from("delete me")], "deleteme.txt", { type: "text/plain" });
195
- await controller.uploadFile({ file, fileName: "deleteme.txt", path: "photos" });
198
+ await controller.putObject({ file,
199
+ key: "photos/deleteme.txt" });
196
200
 
197
201
  const res = await app.fetch(
198
202
  new Request("http://localhost/api/storage/file/default/photos/deleteme.txt", { method: "DELETE" })