@beechcms/api 0.6.0-preview.1 → 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 (222) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  4. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  5. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  6. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  7. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  8. package/assets/dashboard/beechLogoDark.svg +48 -48
  9. package/assets/dashboard/index.html +18 -18
  10. package/assets/dashboard/noResult.svg +42 -42
  11. package/assets/dashboard/sol.svg +3 -3
  12. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  13. package/migrations/0000_v040_base.sql +265 -265
  14. package/migrations/0029_automations.sql +14 -14
  15. package/migrations/0030_test_seeds.sql +125 -125
  16. package/package.json +2 -2
  17. package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
  18. package/src/auth/__fixtures__/static-token-service.ts +22 -22
  19. package/src/auth/bcrypt-hash-provider.ts +24 -24
  20. package/src/auth/constants.ts +14 -14
  21. package/src/auth/generate-refresh-token.test.ts +23 -23
  22. package/src/auth/hash-provider.test.ts +55 -55
  23. package/src/auth/jose-token-service.ts +62 -62
  24. package/src/auth/jwt-claims-passthrough.test.ts +87 -87
  25. package/src/auth/login.test.ts +100 -100
  26. package/src/auth/login.ts +78 -78
  27. package/src/auth/refresh.ts +11 -11
  28. package/src/auth/token-service.test.ts +90 -90
  29. package/src/factory.ts +370 -368
  30. package/src/features/automations/__tests__/action-executors.test.ts +255 -255
  31. package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
  32. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
  33. package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
  34. package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
  35. package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
  36. package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
  37. package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
  38. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
  39. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
  40. package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
  41. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
  42. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
  43. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
  44. package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
  45. package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
  46. package/src/features/automations/action-executors/index.ts +37 -37
  47. package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
  48. package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
  49. package/src/features/automations/action-executors/webhook.executor.ts +67 -67
  50. package/src/features/automations/automation-runner.ts +85 -85
  51. package/src/features/automations/automation-runner.utils.ts +47 -47
  52. package/src/features/automations/automations.handler.ts +197 -197
  53. package/src/features/automations/automations.schema.ts +177 -177
  54. package/src/features/automations/context-resolver.ts +152 -152
  55. package/src/features/automations/cron-runner.ts +140 -140
  56. package/src/features/automations/cron-runner.utils.ts +44 -44
  57. package/src/features/automations/filter-translation.ts +46 -46
  58. package/src/features/automations/index.ts +16 -16
  59. package/src/features/automations/template-grammar.ts +245 -245
  60. package/src/features/automations/var-access-resolver.ts +140 -140
  61. package/src/features/automations/when-evaluator.ts +87 -87
  62. package/src/features/automations/when-pushdown.ts +57 -57
  63. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
  64. package/src/features/backrefs/backrefs.handler.ts +168 -168
  65. package/src/features/backrefs/d1-backref.repository.ts +84 -84
  66. package/src/features/backrefs/index.ts +5 -5
  67. package/src/features/content/constants.ts +16 -16
  68. package/src/features/content/handlers/bulk.handler.ts +185 -185
  69. package/src/features/content/handlers/create.ts +129 -129
  70. package/src/features/content/handlers/delete.ts +62 -62
  71. package/src/features/content/handlers/facets.ts +49 -49
  72. package/src/features/content/handlers/get.ts +120 -120
  73. package/src/features/content/handlers/helpers.test.ts +180 -180
  74. package/src/features/content/handlers/helpers.ts +93 -93
  75. package/src/features/content/handlers/list.ts +174 -174
  76. package/src/features/content/handlers/update.ts +174 -174
  77. package/src/features/content/index.ts +26 -26
  78. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  79. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  80. package/src/features/dashboard-layout/index.ts +5 -0
  81. package/src/features/draft/draft.handler.ts +195 -195
  82. package/src/features/draft/draft.middleware.ts +66 -66
  83. package/src/features/draft/index.ts +5 -5
  84. package/src/features/email/email.provider.ts +42 -42
  85. package/src/features/email/email.service.ts +98 -98
  86. package/src/features/email/email.types.ts +119 -119
  87. package/src/features/email/index.ts +33 -33
  88. package/src/features/email/providers/resend.ts +67 -67
  89. package/src/features/email/providers/smtp.ts +55 -55
  90. package/src/features/email/templates/automation-mail.ts +19 -19
  91. package/src/features/email/templates/password-changed.ts +63 -63
  92. package/src/features/email/templates/password-reset.ts +68 -68
  93. package/src/features/email/templates/shell.ts +96 -96
  94. package/src/features/notifications/index.ts +5 -5
  95. package/src/features/notifications/notifications.handler.ts +105 -105
  96. package/src/features/password-reset/index.ts +19 -19
  97. package/src/features/password-reset/request.ts +92 -92
  98. package/src/features/password-reset/reset.ts +102 -102
  99. package/src/features/rotate-field/index.ts +5 -5
  100. package/src/features/rotate-field/rotate-field.handler.ts +132 -132
  101. package/src/features/rotate-field/rotate-field.schema.ts +17 -17
  102. package/src/features/schema/schema.handler.ts +131 -131
  103. package/src/features/seeds/index.ts +5 -5
  104. package/src/features/seeds/seeds.handler.test.ts +1199 -632
  105. package/src/features/seeds/seeds.handler.ts +558 -693
  106. package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
  107. package/src/features/settings/settings.handler.ts +392 -392
  108. package/src/features/setup/index.ts +288 -204
  109. package/src/features/stats/index.ts +5 -5
  110. package/src/features/stats/stats.handler.ts +420 -420
  111. package/src/features/webhooks/index.ts +63 -63
  112. package/src/index.ts +65 -65
  113. package/src/media-utils.ts +82 -82
  114. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  115. package/src/middleware/auth-providers.middleware.ts +45 -45
  116. package/src/middleware/observability.middleware.test.ts +93 -0
  117. package/src/middleware/observability.middleware.ts +70 -70
  118. package/src/middleware/rate-limit.middleware.ts +45 -45
  119. package/src/middleware/repository.middleware.ts +117 -114
  120. package/src/middleware/seed-registry.middleware.test.ts +97 -97
  121. package/src/middleware/seed-registry.middleware.ts +21 -21
  122. package/src/middleware/storage.middleware.ts +25 -25
  123. package/src/middleware.ts +45 -45
  124. package/src/public/access-policy.ts +27 -27
  125. package/src/public/api-key-middleware.ts +57 -57
  126. package/src/public/cache-utils.ts +38 -38
  127. package/src/public/entry-projection.ts +46 -46
  128. package/src/public/idempotency.ts +23 -23
  129. package/src/public/index.ts +16 -16
  130. package/src/public/problem-details.ts +124 -124
  131. package/src/public/public-add.ts +114 -114
  132. package/src/public/public-edit.ts +163 -163
  133. package/src/public/public-errors.ts +19 -19
  134. package/src/public/public-read.ts +63 -63
  135. package/src/public/public-routes.ts +88 -88
  136. package/src/public/query-builder.test.ts +224 -224
  137. package/src/public/query-builder.ts +156 -156
  138. package/src/public/rate-limit-middleware.ts +34 -34
  139. package/src/public/read-list.ts +54 -54
  140. package/src/public/read-single.ts +48 -48
  141. package/src/public/response-builder.ts +30 -30
  142. package/src/public/sanitize.ts +69 -69
  143. package/src/public/slug-utils.ts +18 -18
  144. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
  145. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
  146. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
  147. package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
  148. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
  149. package/src/rate-limit/no-op-rate-limiter.ts +11 -11
  150. package/src/search-utils.test.ts +211 -211
  151. package/src/search-utils.ts +213 -213
  152. package/src/search.ts +65 -65
  153. package/src/shared/apply-policies.test.ts +81 -81
  154. package/src/shared/apply-policies.ts +67 -67
  155. package/src/shared/automations.repository.d1.ts +150 -150
  156. package/src/shared/background-notification-service.test.ts +62 -62
  157. package/src/shared/background-notification-service.ts +52 -52
  158. package/src/shared/base.repository.d1.ts +32 -32
  159. package/src/shared/content-utils.test.ts +165 -165
  160. package/src/shared/content-utils.ts +86 -86
  161. package/src/shared/content.repository.d1.test.ts +507 -458
  162. package/src/shared/content.repository.d1.ts +851 -851
  163. package/src/shared/d1-activity-log.repository.test.ts +140 -140
  164. package/src/shared/d1-activity-log.repository.ts +105 -105
  165. package/src/shared/d1-activity-logger.test.ts +86 -86
  166. package/src/shared/d1-activity-logger.ts +67 -67
  167. package/src/shared/d1-analytics.repository.test.ts +78 -78
  168. package/src/shared/d1-analytics.repository.ts +85 -85
  169. package/src/shared/d1-content-scan.repository.test.ts +80 -80
  170. package/src/shared/d1-content-scan.repository.ts +37 -37
  171. package/src/shared/d1-notification.repository.test.ts +128 -128
  172. package/src/shared/d1-notification.repository.ts +118 -118
  173. package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
  174. package/src/shared/d1-password-reset-token.repository.ts +56 -56
  175. package/src/shared/d1-search.repository.test.ts +87 -87
  176. package/src/shared/d1-search.repository.ts +88 -88
  177. package/src/shared/d1-session.repository.test.ts +125 -125
  178. package/src/shared/d1-session.repository.ts +102 -102
  179. package/src/shared/d1-setup-checklist.repository.ts +36 -36
  180. package/src/shared/d1-user.repository.test.ts +151 -151
  181. package/src/shared/d1-user.repository.ts +119 -119
  182. package/src/shared/d1-widget.repository.test.ts +267 -221
  183. package/src/shared/d1-widget.repository.ts +385 -341
  184. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  185. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  186. package/src/shared/demo-data-sql.ts +216 -54
  187. package/src/shared/demo-data.repository.d1.ts +20 -20
  188. package/src/shared/execution-context-scheduler.ts +13 -13
  189. package/src/shared/fixed-clock.ts +25 -25
  190. package/src/shared/fts-sync.ts +8 -8
  191. package/src/shared/idempotency.repository.d1.test.ts +83 -83
  192. package/src/shared/idempotency.repository.d1.ts +60 -60
  193. package/src/shared/in-memory-activity-logger.ts +19 -19
  194. package/src/shared/in-memory-notification-service.ts +19 -19
  195. package/src/shared/in-memory-seed.repository.ts +58 -51
  196. package/src/shared/media.repository.d1.test.ts +107 -107
  197. package/src/shared/media.repository.d1.ts +68 -68
  198. package/src/shared/qstash-notification-service.test.ts +67 -67
  199. package/src/shared/qstash-notification-service.ts +55 -55
  200. package/src/shared/query-utils.ts +141 -141
  201. package/src/shared/request-utils.ts +26 -26
  202. package/src/shared/schema-mutator.d1.test.ts +142 -0
  203. package/src/shared/schema-mutator.d1.ts +64 -64
  204. package/src/shared/seed-layout.repository.d1.test.ts +114 -114
  205. package/src/shared/seed-layout.repository.d1.ts +62 -62
  206. package/src/shared/seed-registry-cache.test.ts +68 -68
  207. package/src/shared/seed-registry-cache.ts +49 -49
  208. package/src/shared/seed.repository.d1.test.ts +143 -143
  209. package/src/shared/seed.repository.d1.ts +98 -98
  210. package/src/shared/sequential-id-generator.ts +33 -33
  211. package/src/shared/site-settings.repository.d1.ts +53 -53
  212. package/src/shared/storage/factory.ts +41 -41
  213. package/src/shared/storage/s3-bucket.ts +195 -195
  214. package/src/shared/storage-utils.ts +40 -40
  215. package/src/shared/system-stats.repository.d1.test.ts +58 -58
  216. package/src/shared/system-stats.repository.d1.ts +48 -48
  217. package/src/types.ts +90 -87
  218. package/src/upload.ts +198 -198
  219. package/src/widget.test.ts +85 -0
  220. package/src/widget.ts +251 -212
  221. package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
  222. package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
@@ -1,14 +1,14 @@
1
- CREATE TABLE IF NOT EXISTS automations (
2
- id TEXT NOT NULL PRIMARY KEY,
3
- seed_slug TEXT NOT NULL,
4
- name TEXT NOT NULL,
5
- enabled INTEGER NOT NULL DEFAULT 1,
6
- triggers TEXT NOT NULL DEFAULT '[]',
7
- trigger_conditions TEXT,
8
- actions TEXT NOT NULL,
9
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
10
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
11
- );
12
-
13
- CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
14
- CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
1
+ CREATE TABLE IF NOT EXISTS automations (
2
+ id TEXT NOT NULL PRIMARY KEY,
3
+ seed_slug TEXT NOT NULL,
4
+ name TEXT NOT NULL,
5
+ enabled INTEGER NOT NULL DEFAULT 1,
6
+ triggers TEXT NOT NULL DEFAULT '[]',
7
+ trigger_conditions TEXT,
8
+ actions TEXT NOT NULL,
9
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
10
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
11
+ );
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
14
+ CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
@@ -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.6.0-preview.1",
3
+ "version": "0.6.0-preview.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/factory.ts"
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "@aws-sdk/client-s3": "^3.995.0",
27
27
  "@aws-sdk/s3-request-presigner": "^3.995.0",
28
- "@beechcms/core": "^0.6.0-preview.1",
28
+ "@beechcms/core": "^0.6.0-preview.2",
29
29
  "@upstash/qstash": "^2.11.0",
30
30
  "bcryptjs": "^2.4.3",
31
31
  "hono": "^4.12.21",
@@ -1,17 +1,17 @@
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
+ // 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,22 +1,22 @@
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
+ // 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,24 +1,24 @@
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
+ // 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,14 +1,14 @@
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
+ // 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,23 +1,23 @@
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
+ // 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,55 +1,55 @@
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
- })
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
+ })