@beechcms/api 0.5.0 → 0.6.0-preview.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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,125 +1,125 @@
1
- -- content_products
2
- CREATE TABLE IF NOT EXISTS content_products (
3
- id TEXT NOT NULL PRIMARY KEY,
4
- slug TEXT NOT NULL UNIQUE,
5
- status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
6
- name TEXT NOT NULL,
7
- description TEXT,
8
- price REAL,
9
- inventory REAL,
10
- rating REAL,
11
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
12
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
13
- );
14
-
15
- CREATE INDEX IF NOT EXISTS idx_products_status ON content_products(status);
16
- CREATE INDEX IF NOT EXISTS idx_products_created_at ON content_products(created_at);
17
- CREATE INDEX IF NOT EXISTS idx_products_name ON content_products(name);
18
- CREATE INDEX IF NOT EXISTS idx_products_price ON content_products(price);
19
- CREATE INDEX IF NOT EXISTS idx_products_inventory ON content_products(inventory);
20
- CREATE INDEX IF NOT EXISTS idx_products_rating ON content_products(rating);
21
-
22
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_products USING fts5(
23
- entry_id UNINDEXED,
24
- name,
25
- description,
26
- tokenize = 'unicode61'
27
- );
28
-
29
- CREATE TRIGGER IF NOT EXISTS fts_products_insert
30
- AFTER INSERT ON content_products BEGIN
31
- INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
32
- END;
33
-
34
- CREATE TRIGGER IF NOT EXISTS fts_products_update
35
- AFTER UPDATE OF name, description ON content_products BEGIN
36
- DELETE FROM fts_products WHERE entry_id = old.id;
37
- INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
38
- END;
39
-
40
- CREATE TRIGGER IF NOT EXISTS fts_products_delete
41
- AFTER DELETE ON content_products BEGIN
42
- DELETE FROM fts_products WHERE entry_id = old.id;
43
- END;
44
-
45
- -- content_reviews
46
- CREATE TABLE IF NOT EXISTS content_reviews (
47
- id TEXT NOT NULL PRIMARY KEY,
48
- slug TEXT NOT NULL UNIQUE,
49
- status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
50
- title TEXT NOT NULL,
51
- body TEXT,
52
- score REAL,
53
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
54
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
55
- );
56
-
57
- CREATE INDEX IF NOT EXISTS idx_reviews_status ON content_reviews(status);
58
- CREATE INDEX IF NOT EXISTS idx_reviews_created_at ON content_reviews(created_at);
59
- CREATE INDEX IF NOT EXISTS idx_reviews_title ON content_reviews(title);
60
- CREATE INDEX IF NOT EXISTS idx_reviews_body ON content_reviews(body);
61
- CREATE INDEX IF NOT EXISTS idx_reviews_score ON content_reviews(score);
62
-
63
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_reviews USING fts5(
64
- entry_id UNINDEXED,
65
- title,
66
- body,
67
- tokenize = 'unicode61'
68
- );
69
-
70
- CREATE TRIGGER IF NOT EXISTS fts_reviews_insert
71
- AFTER INSERT ON content_reviews BEGIN
72
- INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
73
- END;
74
-
75
- CREATE TRIGGER IF NOT EXISTS fts_reviews_update
76
- AFTER UPDATE OF title, body ON content_reviews BEGIN
77
- DELETE FROM fts_reviews WHERE entry_id = old.id;
78
- INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
79
- END;
80
-
81
- CREATE TRIGGER IF NOT EXISTS fts_reviews_delete
82
- AFTER DELETE ON content_reviews BEGIN
83
- DELETE FROM fts_reviews WHERE entry_id = old.id;
84
- END;
85
-
86
- -- content_tasks
87
- CREATE TABLE IF NOT EXISTS content_tasks (
88
- id TEXT NOT NULL PRIMARY KEY,
89
- slug TEXT NOT NULL UNIQUE,
90
- status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
91
- title TEXT NOT NULL,
92
- taskStatus TEXT,
93
- completion REAL,
94
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
95
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
96
- );
97
-
98
- CREATE INDEX IF NOT EXISTS idx_tasks_status ON content_tasks(status);
99
- CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON content_tasks(created_at);
100
- CREATE INDEX IF NOT EXISTS idx_tasks_title ON content_tasks(title);
101
- CREATE INDEX IF NOT EXISTS idx_tasks_taskStatus ON content_tasks(taskStatus);
102
- CREATE INDEX IF NOT EXISTS idx_tasks_completion ON content_tasks(completion);
103
-
104
- CREATE VIRTUAL TABLE IF NOT EXISTS fts_tasks USING fts5(
105
- entry_id UNINDEXED,
106
- title,
107
- taskStatus,
108
- tokenize = 'unicode61'
109
- );
110
-
111
- CREATE TRIGGER IF NOT EXISTS fts_tasks_insert
112
- AFTER INSERT ON content_tasks BEGIN
113
- INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
114
- END;
115
-
116
- CREATE TRIGGER IF NOT EXISTS fts_tasks_update
117
- AFTER UPDATE OF title, taskStatus ON content_tasks BEGIN
118
- DELETE FROM fts_tasks WHERE entry_id = old.id;
119
- INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
120
- END;
121
-
122
- CREATE TRIGGER IF NOT EXISTS fts_tasks_delete
123
- AFTER DELETE ON content_tasks BEGIN
124
- DELETE FROM fts_tasks WHERE entry_id = old.id;
125
- END;
1
+ -- content_products
2
+ CREATE TABLE IF NOT EXISTS content_products (
3
+ id TEXT NOT NULL PRIMARY KEY,
4
+ slug TEXT NOT NULL UNIQUE,
5
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
6
+ name TEXT NOT NULL,
7
+ description TEXT,
8
+ price REAL,
9
+ inventory REAL,
10
+ rating REAL,
11
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
12
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS idx_products_status ON content_products(status);
16
+ CREATE INDEX IF NOT EXISTS idx_products_created_at ON content_products(created_at);
17
+ CREATE INDEX IF NOT EXISTS idx_products_name ON content_products(name);
18
+ CREATE INDEX IF NOT EXISTS idx_products_price ON content_products(price);
19
+ CREATE INDEX IF NOT EXISTS idx_products_inventory ON content_products(inventory);
20
+ CREATE INDEX IF NOT EXISTS idx_products_rating ON content_products(rating);
21
+
22
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_products USING fts5(
23
+ entry_id UNINDEXED,
24
+ name,
25
+ description,
26
+ tokenize = 'unicode61'
27
+ );
28
+
29
+ CREATE TRIGGER IF NOT EXISTS fts_products_insert
30
+ AFTER INSERT ON content_products BEGIN
31
+ INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
32
+ END;
33
+
34
+ CREATE TRIGGER IF NOT EXISTS fts_products_update
35
+ AFTER UPDATE OF name, description ON content_products BEGIN
36
+ DELETE FROM fts_products WHERE entry_id = old.id;
37
+ INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
38
+ END;
39
+
40
+ CREATE TRIGGER IF NOT EXISTS fts_products_delete
41
+ AFTER DELETE ON content_products BEGIN
42
+ DELETE FROM fts_products WHERE entry_id = old.id;
43
+ END;
44
+
45
+ -- content_reviews
46
+ CREATE TABLE IF NOT EXISTS content_reviews (
47
+ id TEXT NOT NULL PRIMARY KEY,
48
+ slug TEXT NOT NULL UNIQUE,
49
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
50
+ title TEXT NOT NULL,
51
+ body TEXT,
52
+ score REAL,
53
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
54
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_reviews_status ON content_reviews(status);
58
+ CREATE INDEX IF NOT EXISTS idx_reviews_created_at ON content_reviews(created_at);
59
+ CREATE INDEX IF NOT EXISTS idx_reviews_title ON content_reviews(title);
60
+ CREATE INDEX IF NOT EXISTS idx_reviews_body ON content_reviews(body);
61
+ CREATE INDEX IF NOT EXISTS idx_reviews_score ON content_reviews(score);
62
+
63
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_reviews USING fts5(
64
+ entry_id UNINDEXED,
65
+ title,
66
+ body,
67
+ tokenize = 'unicode61'
68
+ );
69
+
70
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_insert
71
+ AFTER INSERT ON content_reviews BEGIN
72
+ INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
73
+ END;
74
+
75
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_update
76
+ AFTER UPDATE OF title, body ON content_reviews BEGIN
77
+ DELETE FROM fts_reviews WHERE entry_id = old.id;
78
+ INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
79
+ END;
80
+
81
+ CREATE TRIGGER IF NOT EXISTS fts_reviews_delete
82
+ AFTER DELETE ON content_reviews BEGIN
83
+ DELETE FROM fts_reviews WHERE entry_id = old.id;
84
+ END;
85
+
86
+ -- content_tasks
87
+ CREATE TABLE IF NOT EXISTS content_tasks (
88
+ id TEXT NOT NULL PRIMARY KEY,
89
+ slug TEXT NOT NULL UNIQUE,
90
+ status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
91
+ title TEXT NOT NULL,
92
+ taskStatus TEXT,
93
+ completion REAL,
94
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
95
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON content_tasks(status);
99
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON content_tasks(created_at);
100
+ CREATE INDEX IF NOT EXISTS idx_tasks_title ON content_tasks(title);
101
+ CREATE INDEX IF NOT EXISTS idx_tasks_taskStatus ON content_tasks(taskStatus);
102
+ CREATE INDEX IF NOT EXISTS idx_tasks_completion ON content_tasks(completion);
103
+
104
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_tasks USING fts5(
105
+ entry_id UNINDEXED,
106
+ title,
107
+ taskStatus,
108
+ tokenize = 'unicode61'
109
+ );
110
+
111
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_insert
112
+ AFTER INSERT ON content_tasks BEGIN
113
+ INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
114
+ END;
115
+
116
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_update
117
+ AFTER UPDATE OF title, taskStatus ON content_tasks BEGIN
118
+ DELETE FROM fts_tasks WHERE entry_id = old.id;
119
+ INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
120
+ END;
121
+
122
+ CREATE TRIGGER IF NOT EXISTS fts_tasks_delete
123
+ AFTER DELETE ON content_tasks BEGIN
124
+ DELETE FROM fts_tasks WHERE entry_id = old.id;
125
+ END;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/api",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-preview.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/factory.ts"
@@ -15,8 +15,8 @@
15
15
  "scripts": {
16
16
  "dev": "wrangler dev --port 8789",
17
17
  "build": "tsc -p tsconfig.build.json --noEmit",
18
- "db:migrate:local": "wrangler d1 execute beech-db --local --file=./migrations/0000_v040_base.sql && wrangler d1 execute beech-db --local --file=./migrations/0029_automations.sql && wrangler d1 execute beech-db --local --file=./migrations/0030_test_seeds.sql",
19
- "db:reset:local": "node -e \"require('fs').rmSync('.wrangler/state', {recursive:true,force:true})\" && npm run db:migrate:local && cd ../.. && node bin/cli.mjs seed:load --local && cd apps/api && wrangler d1 execute beech-db --local --file=./migrations/0028_v040_seed_data.sql",
18
+ "db:migrate:local": "node ./scripts/bootstrap-d1.mjs",
19
+ "db:reset:local": "node -e \"require('fs').rmSync('.wrangler/state', {recursive:true,force:true})\" && node ./scripts/bootstrap-d1.mjs",
20
20
  "deploy": "wrangler deploy --minify",
21
21
  "cf-typegen": "wrangler types --env-interface CloudflareBindings",
22
22
  "test": "vitest run",
@@ -24,16 +24,20 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@aws-sdk/client-s3": "^3.995.0",
27
- "@beechcms/core": "^0.5.0",
27
+ "@aws-sdk/s3-request-presigner": "^3.995.0",
28
+ "@beechcms/core": "^0.6.0-preview.2",
29
+ "@upstash/qstash": "^2.11.0",
28
30
  "bcryptjs": "^2.4.3",
29
- "hono": "^4.11.9",
31
+ "hono": "^4.12.21",
30
32
  "jose": "^6.1.3"
31
33
  },
32
34
  "devDependencies": {
33
35
  "@cloudflare/workers-types": "^4.20260213.0",
34
36
  "@types/bcryptjs": "^2.4.6",
35
- "@vitest/coverage-v8": "^3.2.4",
36
- "vitest": "^3.2.4",
37
- "wrangler": "^4.4.0"
38
- }
37
+ "@vitest/coverage-v8": "^4.1.0",
38
+ "vitest": "^4.1.0",
39
+ "wrangler": "^4.98.0"
40
+ },
41
+ "license": "BUSL-1.1",
42
+ "licenseFile": "../../LICENSE"
39
43
  }
@@ -1,13 +1,17 @@
1
- import type { IHashProvider } from '@beechcms/core'
2
-
3
- const HASH_PREFIX = 'HASH:'
4
-
5
- export class InMemoryHashProvider implements IHashProvider {
6
- async hash(plaintextPassword: string): Promise<string> {
7
- return HASH_PREFIX + plaintextPassword
8
- }
9
-
10
- async verify(plaintextPassword: string, storedHash: string): Promise<boolean> {
11
- return storedHash === HASH_PREFIX + plaintextPassword
12
- }
13
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { IHashProvider } from '@beechcms/core'
6
+
7
+ const HASH_PREFIX = 'HASH:'
8
+
9
+ export class InMemoryHashProvider implements IHashProvider {
10
+ async hash(plaintextPassword: string): Promise<string> {
11
+ return HASH_PREFIX + plaintextPassword
12
+ }
13
+
14
+ async verify(plaintextPassword: string, storedHash: string): Promise<boolean> {
15
+ return storedHash === HASH_PREFIX + plaintextPassword
16
+ }
17
+ }
@@ -1,18 +1,22 @@
1
- import type { ITokenService, IssueTokenOptions, JwtClaims } from '@beechcms/core'
2
-
3
- const TEST_TOKEN_PREFIX = 'test:'
4
-
5
- export class StaticTokenService implements ITokenService {
6
- private readonly issuedClaims = new Map<string, JwtClaims>()
7
-
8
- async issue(claims: JwtClaims, _options?: IssueTokenOptions): Promise<string> {
9
- const token = TEST_TOKEN_PREFIX + claims.sub
10
- this.issuedClaims.set(token, claims)
11
- return token
12
- }
13
-
14
- async verify(token: string): Promise<JwtClaims | null> {
15
- if (!token.startsWith(TEST_TOKEN_PREFIX)) return null
16
- return this.issuedClaims.get(token) ?? null
17
- }
18
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import type { ITokenService, IssueTokenOptions, JwtClaims } from '@beechcms/core'
6
+
7
+ const TEST_TOKEN_PREFIX = 'test:'
8
+
9
+ export class StaticTokenService implements ITokenService {
10
+ private readonly issuedClaims = new Map<string, JwtClaims>()
11
+
12
+ async issue(claims: JwtClaims, _options?: IssueTokenOptions): Promise<string> {
13
+ const token = TEST_TOKEN_PREFIX + claims.sub
14
+ this.issuedClaims.set(token, claims)
15
+ return token
16
+ }
17
+
18
+ async verify(token: string): Promise<JwtClaims | null> {
19
+ if (!token.startsWith(TEST_TOKEN_PREFIX)) return null
20
+ return this.issuedClaims.get(token) ?? null
21
+ }
22
+ }
@@ -1,20 +1,24 @@
1
- import bcrypt from 'bcryptjs'
2
- import type { IHashProvider } from '@beechcms/core'
3
-
4
- const DEFAULT_BCRYPT_ROUNDS = 10
5
-
6
- export class BcryptHashProvider implements IHashProvider {
7
- private readonly saltRounds: number
8
-
9
- constructor(saltRounds: number = DEFAULT_BCRYPT_ROUNDS) {
10
- this.saltRounds = saltRounds
11
- }
12
-
13
- async hash(plaintextPassword: string): Promise<string> {
14
- return bcrypt.hash(plaintextPassword, this.saltRounds)
15
- }
16
-
17
- async verify(plaintextPassword: string, storedHash: string): Promise<boolean> {
18
- return bcrypt.compare(plaintextPassword, storedHash)
19
- }
20
- }
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import bcrypt from 'bcryptjs'
6
+ import type { IHashProvider } from '@beechcms/core'
7
+
8
+ export const BCRYPT_SALT_ROUNDS = 10
9
+
10
+ export class BcryptHashProvider implements IHashProvider {
11
+ private readonly saltRounds: number
12
+
13
+ constructor(saltRounds: number = BCRYPT_SALT_ROUNDS) {
14
+ this.saltRounds = saltRounds
15
+ }
16
+
17
+ async hash(plaintextPassword: string): Promise<string> {
18
+ return bcrypt.hash(plaintextPassword, this.saltRounds)
19
+ }
20
+
21
+ async verify(plaintextPassword: string, storedHash: string): Promise<boolean> {
22
+ return bcrypt.compare(plaintextPassword, storedHash)
23
+ }
24
+ }
@@ -1,10 +1,14 @@
1
- /** API auth error messages - used by handlers and tests */
2
- export const AUTH_ERRORS = {
3
- INVALID_REQUEST: 'Invalid request',
4
- INVALID_CREDENTIALS: 'Invalid credentials',
5
- DATABASE_ERROR: 'Database error',
6
- /** Generic message for 500 errors - does not reveal system details */
7
- GENERIC_ERROR: 'An error occurred',
8
- /** Rate limit exceeded */
9
- RATE_LIMIT_EXCEEDED: 'Too many requests',
10
- } as const
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ /** API auth error messages - used by handlers and tests */
6
+ export const AUTH_ERRORS = {
7
+ INVALID_REQUEST: 'Invalid request',
8
+ INVALID_CREDENTIALS: 'Invalid credentials',
9
+ DATABASE_ERROR: 'Database error',
10
+ /** Generic message for 500 errors - does not reveal system details */
11
+ GENERIC_ERROR: 'An error occurred',
12
+ /** Rate limit exceeded */
13
+ RATE_LIMIT_EXCEEDED: 'Too many requests',
14
+ } as const
@@ -1,19 +1,23 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { generateRefreshToken } from './refresh'
3
-
4
- describe('generateRefreshToken', () => {
5
- it('returns a non-empty string', () => {
6
- expect(generateRefreshToken().length).toBeGreaterThan(0)
7
- })
8
-
9
- it('returns a UUID-shaped value (8-4-4-4-12 hex groups)', () => {
10
- expect(generateRefreshToken()).toMatch(
11
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
12
- )
13
- })
14
-
15
- it('returns a different token on every call', () => {
16
- const tokens = new Set(Array.from({ length: 10 }, () => generateRefreshToken()))
17
- expect(tokens.size).toBe(10)
18
- })
19
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { generateRefreshToken } from './refresh'
7
+
8
+ describe('generateRefreshToken', () => {
9
+ it('returns a non-empty string', () => {
10
+ expect(generateRefreshToken().length).toBeGreaterThan(0)
11
+ })
12
+
13
+ it('returns a 64-character hex string (256-bit / 32 bytes of entropy)', () => {
14
+ const token = generateRefreshToken()
15
+ expect(token).toHaveLength(64)
16
+ expect(token).toMatch(/^[0-9a-f]+$/)
17
+ })
18
+
19
+ it('returns a different token on every call', () => {
20
+ const tokens = new Set(Array.from({ length: 10 }, () => generateRefreshToken()))
21
+ expect(tokens.size).toBe(10)
22
+ })
23
+ })
@@ -1,46 +1,55 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { InMemoryHashProvider } from './in-memory-hash-provider'
3
- import { BcryptHashProvider } from './bcrypt-hash-provider'
4
-
5
- describe('InMemoryHashProvider', () => {
6
- const provider = new InMemoryHashProvider()
7
-
8
- it('hash returns the HASH: prefix concatenated with the plaintext', async () => {
9
- expect(await provider.hash('mypassword')).toBe('HASH:mypassword')
10
- })
11
-
12
- it('verify returns true when the plaintext matches the stored hash', async () => {
13
- const hash = await provider.hash('correct')
14
- expect(await provider.verify('correct', hash)).toBe(true)
15
- })
16
-
17
- it('verify returns false when the plaintext does not match', async () => {
18
- const hash = await provider.hash('correct')
19
- expect(await provider.verify('wrong', hash)).toBe(false)
20
- })
21
-
22
- it('verify returns false for a string that does not follow the HASH: format', async () => {
23
- expect(await provider.verify('anything', 'not-a-prefixed-hash')).toBe(false)
24
- })
25
- })
26
-
27
- describe('BcryptHashProvider', () => {
28
- // Use 4 salt rounds so the test runs in milliseconds.
29
- const provider = new BcryptHashProvider(4)
30
-
31
- it('hash and verify roundtrip: correct password returns true', async () => {
32
- const hash = await provider.hash('secure-password')
33
- expect(await provider.verify('secure-password', hash)).toBe(true)
34
- })
35
-
36
- it('verify returns false for an incorrect password', async () => {
37
- const hash = await provider.hash('correct')
38
- expect(await provider.verify('incorrect', hash)).toBe(false)
39
- })
40
-
41
- it('verify returns false against the DUMMY_PASSWORD_HASH for any plaintext', async () => {
42
- // The dummy hash is used for timing-attack mitigation; it must not match real passwords.
43
- const dummyHash = '$2a$10$SbkRFOafACxVM2ahxerVDu3tSkCXWm29b62WdB.4WGG02Qjsfzni6'
44
- expect(await provider.verify('password123', dummyHash)).toBe(false)
45
- })
46
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect } from 'vitest'
6
+ import { InMemoryHashProvider } from './__fixtures__/in-memory-hash-provider'
7
+ import { BcryptHashProvider, BCRYPT_SALT_ROUNDS } from './bcrypt-hash-provider'
8
+ import { DUMMY_PASSWORD_HASH } from './login'
9
+
10
+ describe('InMemoryHashProvider', () => {
11
+ const provider = new InMemoryHashProvider()
12
+
13
+ it('hash returns the HASH: prefix concatenated with the plaintext', async () => {
14
+ expect(await provider.hash('mypassword')).toBe('HASH:mypassword')
15
+ })
16
+
17
+ it('verify returns true when the plaintext matches the stored hash', async () => {
18
+ const hash = await provider.hash('correct')
19
+ expect(await provider.verify('correct', hash)).toBe(true)
20
+ })
21
+
22
+ it('verify returns false when the plaintext does not match', async () => {
23
+ const hash = await provider.hash('correct')
24
+ expect(await provider.verify('wrong', hash)).toBe(false)
25
+ })
26
+
27
+ it('verify returns false for a string that does not follow the HASH: format', async () => {
28
+ expect(await provider.verify('anything', 'not-a-prefixed-hash')).toBe(false)
29
+ })
30
+ })
31
+
32
+ describe('BcryptHashProvider', () => {
33
+ // Use 4 salt rounds so the test runs in milliseconds.
34
+ const provider = new BcryptHashProvider(4)
35
+
36
+ it('hash and verify roundtrip: correct password returns true', async () => {
37
+ const hash = await provider.hash('secure-password')
38
+ expect(await provider.verify('secure-password', hash)).toBe(true)
39
+ })
40
+
41
+ it('verify returns false for an incorrect password', async () => {
42
+ const hash = await provider.hash('correct')
43
+ expect(await provider.verify('incorrect', hash)).toBe(false)
44
+ })
45
+
46
+ it('verify returns false against the DUMMY_PASSWORD_HASH for any plaintext', async () => {
47
+ expect(await provider.verify('password123', DUMMY_PASSWORD_HASH)).toBe(false)
48
+ })
49
+
50
+ it('DUMMY_PASSWORD_HASH bcrypt cost matches BCRYPT_SALT_ROUNDS — timing oracle invariant', () => {
51
+ const match = DUMMY_PASSWORD_HASH.match(/^\$2[aby]\$(\d+)\$/)
52
+ expect(match).not.toBeNull()
53
+ expect(Number(match![1])).toBe(BCRYPT_SALT_ROUNDS)
54
+ })
55
+ })