@mevdragon/vidfarm-devcli 0.2.11 → 0.2.13

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 (199) hide show
  1. package/.env.example +37 -1
  2. package/README.md +27 -183
  3. package/SKILL.director.md +293 -0
  4. package/SKILL.platform.md +312 -0
  5. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +275 -0
  6. package/auto-create-hyperframe-templates/extractor-system-prompt.md +128 -0
  7. package/auto-create-hyperframe-templates/input-manifest.schema.json +56 -0
  8. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +118 -0
  9. package/auto-create-hyperframe-templates/production-graph.schema.json +314 -0
  10. package/auto-create-hyperframe-templates/runbook.md +198 -0
  11. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +368 -0
  12. package/auto-create-hyperframe-templates/template-plan.schema.json +311 -0
  13. package/auto-create-hyperframe-templates/template-planner-prompt.md +144 -0
  14. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +124 -0
  15. package/auto-create-templates/extractor-system-prompt.md +8 -2
  16. package/auto-create-templates/template-plan.schema.json +73 -2
  17. package/auto-create-templates/template-planner-prompt.md +63 -1
  18. package/dist/src/account-pages-legacy.js +361 -89
  19. package/dist/src/account-pages-legacy.js.map +1 -0
  20. package/dist/src/account-pages.js +13 -3
  21. package/dist/src/account-pages.js.map +1 -0
  22. package/dist/src/app.js +7060 -946
  23. package/dist/src/app.js.map +1 -0
  24. package/dist/src/cli.js +111 -29
  25. package/dist/src/cli.js.map +1 -0
  26. package/dist/src/composition-runtime.js +581 -0
  27. package/dist/src/composition-runtime.js.map +1 -0
  28. package/dist/src/config.js +69 -11
  29. package/dist/src/config.js.map +1 -0
  30. package/dist/src/context.js +280 -21
  31. package/dist/src/context.js.map +1 -0
  32. package/dist/src/dev-app-legacy.js +18 -17
  33. package/dist/src/dev-app-legacy.js.map +1 -0
  34. package/dist/src/dev-app.js +1 -0
  35. package/dist/src/dev-app.js.map +1 -0
  36. package/dist/src/domain.js +2 -1
  37. package/dist/src/domain.js.map +1 -0
  38. package/dist/src/editor-chat-history.js +10 -0
  39. package/dist/src/editor-chat-history.js.map +1 -0
  40. package/dist/src/editor-chat.js +133 -24
  41. package/dist/src/editor-chat.js.map +1 -0
  42. package/dist/src/frontend/flockposter-cache-store.js +10 -2
  43. package/dist/src/frontend/flockposter-cache-store.js.map +1 -0
  44. package/dist/src/frontend/homepage-client.js +6 -3
  45. package/dist/src/frontend/homepage-client.js.map +1 -0
  46. package/dist/src/frontend/homepage-shared.js +1 -0
  47. package/dist/src/frontend/homepage-shared.js.map +1 -0
  48. package/dist/src/frontend/homepage-store.js +1 -0
  49. package/dist/src/frontend/homepage-store.js.map +1 -0
  50. package/dist/src/frontend/homepage-view.js +131 -18
  51. package/dist/src/frontend/homepage-view.js.map +1 -0
  52. package/dist/src/frontend/page-runtime-client.js +5 -2
  53. package/dist/src/frontend/page-runtime-client.js.map +1 -0
  54. package/dist/src/frontend/page-runtime-store.js +1 -0
  55. package/dist/src/frontend/page-runtime-store.js.map +1 -0
  56. package/dist/src/frontend/sentry.js +42 -0
  57. package/dist/src/frontend/sentry.js.map +1 -0
  58. package/dist/src/frontend/template-editor-chat.js +1719 -213
  59. package/dist/src/frontend/template-editor-chat.js.map +1 -0
  60. package/dist/src/help-page.js +333 -0
  61. package/dist/src/help-page.js.map +1 -0
  62. package/dist/src/homepage.js +169 -47
  63. package/dist/src/homepage.js.map +1 -0
  64. package/dist/src/hyperframes/composition.js +180 -0
  65. package/dist/src/hyperframes/composition.js.map +1 -0
  66. package/dist/src/index.js +1 -0
  67. package/dist/src/index.js.map +1 -0
  68. package/dist/src/instrument.js +30 -0
  69. package/dist/src/instrument.js.map +1 -0
  70. package/dist/src/lib/crypto.js +1 -0
  71. package/dist/src/lib/crypto.js.map +1 -0
  72. package/dist/src/lib/dev-log.js +54 -0
  73. package/dist/src/lib/dev-log.js.map +1 -0
  74. package/dist/src/lib/display-name.js +11 -0
  75. package/dist/src/lib/display-name.js.map +1 -0
  76. package/dist/src/lib/ids.js +21 -1
  77. package/dist/src/lib/ids.js.map +1 -0
  78. package/dist/src/lib/images.js +1 -0
  79. package/dist/src/lib/images.js.map +1 -0
  80. package/dist/src/lib/json.js +1 -0
  81. package/dist/src/lib/json.js.map +1 -0
  82. package/dist/src/lib/template-dna.js +1 -0
  83. package/dist/src/lib/template-dna.js.map +1 -0
  84. package/dist/src/lib/template-paths.js +1 -0
  85. package/dist/src/lib/template-paths.js.map +1 -0
  86. package/dist/src/lib/template-style-options.js +40 -2
  87. package/dist/src/lib/template-style-options.js.map +1 -0
  88. package/dist/src/lib/time.js +1 -0
  89. package/dist/src/lib/time.js.map +1 -0
  90. package/dist/src/lib/video-quality-harness.js +60 -0
  91. package/dist/src/lib/video-quality-harness.js.map +1 -0
  92. package/dist/src/page-runtime.js +1 -0
  93. package/dist/src/page-runtime.js.map +1 -0
  94. package/dist/src/page-shell.js +1501 -49
  95. package/dist/src/page-shell.js.map +1 -0
  96. package/dist/src/primitive-context.js +144 -8
  97. package/dist/src/primitive-context.js.map +1 -0
  98. package/dist/src/primitive-registry.js +1919 -78
  99. package/dist/src/primitive-registry.js.map +1 -0
  100. package/dist/src/primitive-sdk.js +1 -0
  101. package/dist/src/primitive-sdk.js.map +1 -0
  102. package/dist/src/primitives/remotion/html-image.js +3 -1
  103. package/dist/src/primitives/remotion/html-image.js.map +1 -0
  104. package/dist/src/primitives/remotion/media-slideshow.js +60 -0
  105. package/dist/src/primitives/remotion/media-slideshow.js.map +1 -0
  106. package/dist/src/react-page-shell.js +1 -0
  107. package/dist/src/react-page-shell.js.map +1 -0
  108. package/dist/src/ready-post-schedule-component.js +1 -0
  109. package/dist/src/ready-post-schedule-component.js.map +1 -0
  110. package/dist/src/registry.js +237 -24
  111. package/dist/src/registry.js.map +1 -0
  112. package/dist/src/runtime.js +3 -0
  113. package/dist/src/runtime.js.map +1 -0
  114. package/dist/src/services/api-call-history.js +4 -0
  115. package/dist/src/services/api-call-history.js.map +1 -0
  116. package/dist/src/services/auth.js +40 -50
  117. package/dist/src/services/auth.js.map +1 -0
  118. package/dist/src/services/billing.js +20 -44
  119. package/dist/src/services/billing.js.map +1 -0
  120. package/dist/src/services/chat-threads.js +10 -6
  121. package/dist/src/services/chat-threads.js.map +1 -0
  122. package/dist/src/services/fork-access.js +94 -0
  123. package/dist/src/services/fork-access.js.map +1 -0
  124. package/dist/src/services/ghostcut.js +132 -0
  125. package/dist/src/services/ghostcut.js.map +1 -0
  126. package/dist/src/services/hyperframes.js +1014 -0
  127. package/dist/src/services/hyperframes.js.map +1 -0
  128. package/dist/src/services/job-capacity.js +14 -0
  129. package/dist/src/services/job-capacity.js.map +1 -0
  130. package/dist/src/services/job-logs.js +4 -0
  131. package/dist/src/services/job-logs.js.map +1 -0
  132. package/dist/src/services/jobs.js +99 -91
  133. package/dist/src/services/jobs.js.map +1 -0
  134. package/dist/src/services/media-processing.js +743 -0
  135. package/dist/src/services/media-processing.js.map +1 -0
  136. package/dist/src/services/primitive-media-lambda.js +280 -0
  137. package/dist/src/services/primitive-media-lambda.js.map +1 -0
  138. package/dist/src/services/providers.js +1560 -178
  139. package/dist/src/services/providers.js.map +1 -0
  140. package/dist/src/services/rate-limits.js +3 -2
  141. package/dist/src/services/rate-limits.js.map +1 -0
  142. package/dist/src/services/remotion.js +495 -92
  143. package/dist/src/services/remotion.js.map +1 -0
  144. package/dist/src/services/serverless-auth.js +374 -0
  145. package/dist/src/services/serverless-auth.js.map +1 -0
  146. package/dist/src/services/serverless-jobs.js +1074 -0
  147. package/dist/src/services/serverless-jobs.js.map +1 -0
  148. package/dist/src/services/serverless-provider-keys.js +401 -0
  149. package/dist/src/services/serverless-provider-keys.js.map +1 -0
  150. package/dist/src/services/serverless-records.js +1086 -0
  151. package/dist/src/services/serverless-records.js.map +1 -0
  152. package/dist/src/services/serverless-template-configs.js +67 -0
  153. package/dist/src/services/serverless-template-configs.js.map +1 -0
  154. package/dist/src/services/storage.js +170 -35
  155. package/dist/src/services/storage.js.map +1 -0
  156. package/dist/src/services/template-certification.js +295 -3
  157. package/dist/src/services/template-certification.js.map +1 -0
  158. package/dist/src/services/template-loader.js +45 -1
  159. package/dist/src/services/template-loader.js.map +1 -0
  160. package/dist/src/services/template-runtime-bundles.js +217 -0
  161. package/dist/src/services/template-runtime-bundles.js.map +1 -0
  162. package/dist/src/services/template-sources.js +452 -87
  163. package/dist/src/services/template-sources.js.map +1 -0
  164. package/dist/src/services/video-normalization.js +2 -0
  165. package/dist/src/services/video-normalization.js.map +1 -0
  166. package/dist/src/services/webhooks.js +7 -6
  167. package/dist/src/services/webhooks.js.map +1 -0
  168. package/dist/src/template-editor-pages.js +5637 -1322
  169. package/dist/src/template-editor-pages.js.map +1 -0
  170. package/dist/src/template-editor-shell.js +1376 -170
  171. package/dist/src/template-editor-shell.js.map +1 -0
  172. package/dist/src/template-sdk.js +1 -0
  173. package/dist/src/template-sdk.js.map +1 -0
  174. package/dist/src/worker.js +11 -226
  175. package/dist/src/worker.js.map +1 -0
  176. package/package.json +47 -12
  177. package/public/assets/homepage-client-app.js +34 -8
  178. package/public/assets/page-runtime-client-app.js +44 -18
  179. package/readme.secret.md +89 -0
  180. package/templates/vidfarm_template_0000/README.md +106 -0
  181. package/templates/vidfarm_template_0000/SKILL.md +266 -0
  182. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  183. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  184. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  185. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  186. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  187. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  188. package/templates/vidfarm_template_0000/composition.json +11 -0
  189. package/templates/vidfarm_template_0000/src/lib/images.js +221 -0
  190. package/templates/vidfarm_template_0000/src/remotion/Root.js +33 -0
  191. package/templates/vidfarm_template_0000/src/remotion/index.js +3 -0
  192. package/templates/vidfarm_template_0000/src/sdk.js +3 -0
  193. package/templates/vidfarm_template_0000/src/style-options.js +200 -0
  194. package/templates/vidfarm_template_0000/src/template-dna.js +9 -0
  195. package/templates/vidfarm_template_0000/src/template.js +1566 -0
  196. package/templates/vidfarm_template_0000/template.config.json +21 -0
  197. package/GETTING_STARTED.developers.md +0 -87
  198. package/SKILL.developer.md +0 -1177
  199. package/dist/src/db.js +0 -2007
package/dist/src/db.js DELETED
@@ -1,2007 +0,0 @@
1
- import Database from "better-sqlite3";
2
- import { config } from "./config.js";
3
- import { createId } from "./lib/ids.js";
4
- import { parseJson, stringifyJson } from "./lib/json.js";
5
- import { nowIso } from "./lib/time.js";
6
- const db = new Database(config.VIDFARM_DB_PATH);
7
- db.pragma("journal_mode = WAL");
8
- db.pragma("foreign_keys = ON");
9
- db.exec(`
10
- create table if not exists customers (
11
- id text primary key,
12
- email text not null unique,
13
- name text,
14
- default_webhook_url text,
15
- is_developer integer not null default 0,
16
- is_paid_plan integer not null default 0,
17
- plan_tier text not null default 'starter',
18
- about text,
19
- groupchat_url text,
20
- flockposter_api_key text,
21
- password_hash text,
22
- created_at text not null,
23
- updated_at text not null
24
- );
25
-
26
- create table if not exists api_keys (
27
- id text primary key,
28
- customer_id text not null references customers(id) on delete cascade,
29
- key_hash text not null unique,
30
- raw_value text,
31
- label text,
32
- status text not null,
33
- last_used_at text,
34
- created_at text not null,
35
- updated_at text not null
36
- );
37
-
38
- create table if not exists otp_challenges (
39
- id text primary key,
40
- email text not null,
41
- code_hash text not null,
42
- expires_at text not null,
43
- consumed_at text,
44
- created_at text not null
45
- );
46
-
47
- create table if not exists customer_provider_keys (
48
- id text primary key,
49
- customer_id text not null references customers(id) on delete cascade,
50
- provider text not null,
51
- label text,
52
- encrypted_secret text not null,
53
- status text not null,
54
- last_used_at text,
55
- cooldown_until text,
56
- disabled_reason text,
57
- created_at text not null,
58
- updated_at text not null
59
- );
60
-
61
- create table if not exists provider_key_leases (
62
- key_id text primary key references customer_provider_keys(id) on delete cascade,
63
- lease_token text not null,
64
- worker_id text not null,
65
- job_id text not null,
66
- leased_at text not null,
67
- expires_at text not null
68
- );
69
-
70
- create table if not exists provider_key_waiters (
71
- id text primary key,
72
- customer_id text not null references customers(id) on delete cascade,
73
- provider text not null,
74
- worker_id text not null,
75
- owner_id text not null,
76
- priority integer not null default 100,
77
- created_at text not null,
78
- expires_at text not null
79
- );
80
-
81
- create index if not exists idx_provider_key_waiters_scope on provider_key_waiters(customer_id, provider, priority, created_at);
82
-
83
- create table if not exists provider_key_usage_events (
84
- id text primary key,
85
- key_id text not null references customer_provider_keys(id) on delete cascade,
86
- job_id text not null,
87
- provider text not null,
88
- model text,
89
- event_type text not null,
90
- input_tokens integer,
91
- output_tokens integer,
92
- cost_usd real,
93
- metadata text,
94
- created_at text not null
95
- );
96
-
97
- create table if not exists template_configs (
98
- id text primary key,
99
- customer_id text not null references customers(id) on delete cascade,
100
- template_id text not null,
101
- config_json text not null,
102
- created_at text not null,
103
- updated_at text not null,
104
- unique(customer_id, template_id)
105
- );
106
-
107
- create table if not exists jobs (
108
- id text primary key,
109
- template_id text not null,
110
- operation_name text not null,
111
- workflow_name text not null,
112
- tracer text not null,
113
- status text not null,
114
- customer_id text not null references customers(id) on delete cascade,
115
- payload_json text not null,
116
- result_json text,
117
- error_json text,
118
- progress real not null default 0,
119
- webhook_url text,
120
- parent_job_id text references jobs(id) on delete set null,
121
- priority integer not null default 100,
122
- attempt_count integer not null default 0,
123
- max_attempts integer not null default 6,
124
- run_after text not null,
125
- created_at text not null,
126
- updated_at text not null,
127
- started_at text,
128
- completed_at text
129
- );
130
-
131
- create index if not exists idx_jobs_runnable on jobs(status, run_after, priority, created_at);
132
-
133
- create table if not exists artifacts (
134
- id text primary key,
135
- job_id text not null references jobs(id) on delete cascade,
136
- customer_id text not null references customers(id) on delete cascade,
137
- template_id text not null,
138
- kind text not null,
139
- storage_key text not null,
140
- public_url text,
141
- metadata_json text not null,
142
- created_at text not null
143
- );
144
-
145
- drop table if exists billing_events;
146
-
147
- create table if not exists customer_credit_grants (
148
- id text primary key,
149
- customer_id text not null references customers(id) on delete cascade,
150
- grant_type text not null,
151
- amount_usd real not null,
152
- metadata_json text not null,
153
- created_at text not null,
154
- unique(customer_id, grant_type)
155
- );
156
-
157
- create index if not exists idx_customer_credit_grants_customer_created on customer_credit_grants(customer_id, created_at desc);
158
-
159
- create table if not exists webhook_deliveries (
160
- id text primary key,
161
- job_id text not null references jobs(id) on delete cascade,
162
- customer_id text not null references customers(id) on delete cascade,
163
- event_type text not null,
164
- destination_url text not null,
165
- payload_json text not null,
166
- status text not null,
167
- attempt_count integer not null default 0,
168
- next_attempt_at text not null,
169
- last_error text,
170
- created_at text not null,
171
- updated_at text not null
172
- );
173
-
174
- create table if not exists user_attachments (
175
- id text primary key,
176
- customer_id text not null references customers(id) on delete cascade,
177
- file_name text not null,
178
- content_type text not null,
179
- size_bytes integer not null,
180
- storage_key text not null,
181
- public_url text,
182
- created_at text not null
183
- );
184
-
185
- create index if not exists idx_user_attachments_customer_created on user_attachments(customer_id, created_at desc);
186
-
187
- create table if not exists email_channels (
188
- id text primary key,
189
- customer_id text not null references customers(id) on delete cascade,
190
- title text not null,
191
- email text not null,
192
- note text,
193
- avatar_url text,
194
- avatar_storage_key text,
195
- status text not null,
196
- verification_token_hash text,
197
- verification_sent_at text,
198
- verified_at text,
199
- created_at text not null,
200
- updated_at text not null,
201
- unique(customer_id, email)
202
- );
203
-
204
- create index if not exists idx_email_channels_customer_created on email_channels(customer_id, created_at desc);
205
- create index if not exists idx_email_channels_token on email_channels(verification_token_hash);
206
-
207
- create table if not exists ready_posts (
208
- id text primary key,
209
- customer_id text not null references customers(id) on delete cascade,
210
- status text not null,
211
- tracer text,
212
- title text,
213
- content text not null,
214
- pinned_comment text,
215
- scheduled_at text,
216
- timezone text,
217
- post_type text not null,
218
- integration_ids_json text not null,
219
- platform_targets_json text not null,
220
- media_assets_json text not null,
221
- renditions_json text not null,
222
- flockposter_payload_json text not null,
223
- field_values_json text not null,
224
- share_password_hash text,
225
- zip_storage_key text,
226
- zip_public_url text,
227
- archived_at text,
228
- deleted_at text,
229
- created_at text not null,
230
- updated_at text not null
231
- );
232
-
233
- create index if not exists idx_ready_posts_customer_created on ready_posts(customer_id, created_at desc);
234
-
235
- create table if not exists post_schedules (
236
- id text primary key,
237
- customer_id text not null references customers(id) on delete cascade,
238
- ready_post_id text references ready_posts(id) on delete cascade,
239
- managed_by text not null,
240
- provider text not null,
241
- destination_type text not null,
242
- destination_id text not null,
243
- scheduled_at text not null,
244
- timezone text,
245
- status text not null,
246
- provider_status text,
247
- provider_schedule_id text,
248
- title text,
249
- summary text,
250
- url text,
251
- additional_notes text,
252
- provider_payload_json text not null,
253
- provider_response_json text not null,
254
- last_synced_at text,
255
- last_error text,
256
- cancelled_at text,
257
- sent_at text,
258
- created_at text not null,
259
- updated_at text not null
260
- );
261
-
262
- create index if not exists idx_post_schedules_customer_scheduled on post_schedules(customer_id, scheduled_at asc);
263
- create index if not exists idx_post_schedules_ready_post on post_schedules(ready_post_id, status);
264
- create index if not exists idx_post_schedules_provider_id on post_schedules(customer_id, provider, provider_schedule_id);
265
-
266
- create table if not exists chat_threads (
267
- id text primary key,
268
- customer_id text not null references customers(id) on delete cascade,
269
- template_id text not null,
270
- title text not null,
271
- storage_prefix text not null,
272
- message_count integer not null default 0,
273
- last_message_at text,
274
- hidden_from_view integer not null default 0,
275
- archived_at text,
276
- created_at text not null,
277
- updated_at text not null
278
- );
279
-
280
- create index if not exists idx_chat_threads_customer_template_updated on chat_threads(customer_id, template_id, updated_at desc);
281
-
282
- create table if not exists chat_thread_tracers (
283
- thread_id text not null references chat_threads(id) on delete cascade,
284
- tracer text not null,
285
- created_at text not null,
286
- primary key (thread_id, tracer)
287
- );
288
-
289
- create index if not exists idx_chat_thread_tracers_tracer on chat_thread_tracers(tracer, thread_id);
290
-
291
- create table if not exists template_sources (
292
- id text primary key,
293
- template_id text not null unique,
294
- slug_id text,
295
- repo_url text not null,
296
- branch text not null,
297
- template_module_path text not null,
298
- skill_path text not null,
299
- install_command text not null,
300
- build_command text not null,
301
- status text not null,
302
- created_at text not null,
303
- updated_at text not null
304
- );
305
-
306
- create table if not exists template_releases (
307
- id text primary key,
308
- source_id text not null references template_sources(id) on delete cascade,
309
- template_id text not null,
310
- branch text not null,
311
- commit_sha text not null,
312
- checkout_path text not null,
313
- skill_path text not null,
314
- module_path text not null,
315
- artifact_manifest_key text,
316
- artifact_key text,
317
- status text not null,
318
- certification_report_json text,
319
- activated_at text,
320
- created_at text not null,
321
- updated_at text not null,
322
- unique(source_id, commit_sha)
323
- );
324
-
325
- create index if not exists idx_template_releases_template_status on template_releases(template_id, status, created_at);
326
- `);
327
- const customerColumns = db.prepare(`pragma table_info(customers)`).all();
328
- if (!customerColumns.some((column) => column.name === "is_developer")) {
329
- db.exec(`alter table customers add column is_developer integer not null default 0;`);
330
- }
331
- if (!customerColumns.some((column) => column.name === "is_paid_plan")) {
332
- db.exec(`alter table customers add column is_paid_plan integer not null default 0;`);
333
- }
334
- if (!customerColumns.some((column) => column.name === "plan_tier")) {
335
- db.exec(`alter table customers add column plan_tier text not null default 'starter';`);
336
- }
337
- db.exec(`
338
- update customers
339
- set plan_tier = 'starter'
340
- where plan_tier in ('standard', 'invisible')
341
- or plan_tier is null
342
- or trim(plan_tier) = ''
343
- `);
344
- if (!customerColumns.some((column) => column.name === "password_hash")) {
345
- db.exec(`alter table customers add column password_hash text;`);
346
- }
347
- if (!customerColumns.some((column) => column.name === "about")) {
348
- db.exec(`alter table customers add column about text;`);
349
- }
350
- if (!customerColumns.some((column) => column.name === "groupchat_url")) {
351
- db.exec(`alter table customers add column groupchat_url text;`);
352
- }
353
- if (!customerColumns.some((column) => column.name === "flockposter_api_key")) {
354
- db.exec(`alter table customers add column flockposter_api_key text;`);
355
- }
356
- const apiKeyColumns = db.prepare(`pragma table_info(api_keys)`).all();
357
- if (!apiKeyColumns.some((column) => column.name === "raw_value")) {
358
- db.exec(`alter table api_keys add column raw_value text;`);
359
- }
360
- const providerKeyColumns = db.prepare(`pragma table_info(customer_provider_keys)`).all();
361
- if (providerKeyColumns.some((column) => column.name === "weight")) {
362
- db.exec(`alter table customer_provider_keys drop column weight;`);
363
- }
364
- const templateSourceColumns = db.prepare(`pragma table_info(template_sources)`).all();
365
- if (!templateSourceColumns.some((column) => column.name === "slug_id")) {
366
- db.exec(`alter table template_sources add column slug_id text;`);
367
- }
368
- db.exec(`
369
- update template_sources
370
- set slug_id = replace(template_id, '-', '_')
371
- where slug_id is null or trim(slug_id) = '';
372
- `);
373
- db.exec(`create unique index if not exists idx_template_sources_slug_id on template_sources(slug_id);`);
374
- db.exec(`
375
- update template_sources
376
- set status = 'approved'
377
- where status = 'active';
378
- `);
379
- db.exec(`
380
- update template_releases
381
- set status = 'pending_approval'
382
- where status = 'certified';
383
- `);
384
- const templateReleaseColumns = db.prepare(`pragma table_info(template_releases)`).all();
385
- if (!templateReleaseColumns.some((column) => column.name === "artifact_manifest_key")) {
386
- db.exec(`alter table template_releases add column artifact_manifest_key text;`);
387
- }
388
- if (!templateReleaseColumns.some((column) => column.name === "artifact_key")) {
389
- db.exec(`alter table template_releases add column artifact_key text;`);
390
- }
391
- const readyPostColumns = db.prepare(`pragma table_info(ready_posts)`).all();
392
- if (!readyPostColumns.some((column) => column.name === "tracer")) {
393
- db.exec(`alter table ready_posts add column tracer text;`);
394
- }
395
- if (!readyPostColumns.some((column) => column.name === "pinned_comment")) {
396
- db.exec(`alter table ready_posts add column pinned_comment text;`);
397
- }
398
- const postScheduleColumns = db.prepare(`pragma table_info(post_schedules)`).all();
399
- if (!postScheduleColumns.some((column) => column.name === "additional_notes")) {
400
- db.exec(`alter table post_schedules add column additional_notes text;`);
401
- }
402
- const emailChannelColumns = db.prepare(`pragma table_info(email_channels)`).all();
403
- if (!emailChannelColumns.some((column) => column.name === "avatar_url")) {
404
- db.exec(`alter table email_channels add column avatar_url text;`);
405
- }
406
- if (!emailChannelColumns.some((column) => column.name === "avatar_storage_key")) {
407
- db.exec(`alter table email_channels add column avatar_storage_key text;`);
408
- }
409
- db.exec(`drop index if exists idx_job_events_job_time;`);
410
- db.exec(`drop table if exists job_events;`);
411
- function mapJob(row) {
412
- return {
413
- id: String(row.id),
414
- templateId: String(row.template_id),
415
- operationName: String(row.operation_name),
416
- workflowName: String(row.workflow_name),
417
- tracer: String(row.tracer),
418
- status: row.status,
419
- customerId: String(row.customer_id),
420
- payload: parseJson(row.payload_json, {}),
421
- result: parseJson(row.result_json, null),
422
- error: parseJson(row.error_json, null),
423
- progress: Number(row.progress),
424
- webhookUrl: row.webhook_url ? String(row.webhook_url) : null,
425
- parentJobId: row.parent_job_id ? String(row.parent_job_id) : null,
426
- priority: Number(row.priority),
427
- attemptCount: Number(row.attempt_count),
428
- maxAttempts: Number(row.max_attempts),
429
- runAfter: String(row.run_after),
430
- createdAt: String(row.created_at),
431
- updatedAt: String(row.updated_at),
432
- startedAt: row.started_at ? String(row.started_at) : null,
433
- completedAt: row.completed_at ? String(row.completed_at) : null
434
- };
435
- }
436
- function mapTemplateSource(row) {
437
- return {
438
- id: String(row.id),
439
- templateId: String(row.template_id),
440
- slugId: String(row.slug_id),
441
- repoUrl: String(row.repo_url),
442
- branch: String(row.branch),
443
- templateModulePath: String(row.template_module_path),
444
- skillPath: String(row.skill_path),
445
- installCommand: String(row.install_command),
446
- buildCommand: String(row.build_command),
447
- status: row.status,
448
- createdAt: String(row.created_at),
449
- updatedAt: String(row.updated_at)
450
- };
451
- }
452
- function mapTemplateRelease(row) {
453
- return {
454
- id: String(row.id),
455
- sourceId: String(row.source_id),
456
- templateId: String(row.template_id),
457
- branch: String(row.branch),
458
- commitSha: String(row.commit_sha),
459
- checkoutPath: String(row.checkout_path),
460
- skillPath: String(row.skill_path),
461
- modulePath: String(row.module_path),
462
- artifactManifestKey: row.artifact_manifest_key ? String(row.artifact_manifest_key) : null,
463
- artifactKey: row.artifact_key ? String(row.artifact_key) : null,
464
- status: row.status,
465
- certificationReport: parseJson(row.certification_report_json, null),
466
- activatedAt: row.activated_at ? String(row.activated_at) : null,
467
- createdAt: String(row.created_at),
468
- updatedAt: String(row.updated_at)
469
- };
470
- }
471
- function mapUserAttachment(row) {
472
- return {
473
- id: String(row.id),
474
- customerId: String(row.customer_id),
475
- fileName: String(row.file_name),
476
- contentType: String(row.content_type),
477
- sizeBytes: Number(row.size_bytes),
478
- storageKey: String(row.storage_key),
479
- publicUrl: row.public_url ? String(row.public_url) : null,
480
- createdAt: String(row.created_at)
481
- };
482
- }
483
- function mapEmailChannel(row) {
484
- const status = String(row.status) === "verified" ? "verified" : "unverified";
485
- return {
486
- id: String(row.id),
487
- customerId: String(row.customer_id),
488
- title: String(row.title),
489
- email: String(row.email),
490
- note: row.note ? String(row.note) : null,
491
- avatarUrl: row.avatar_url ? String(row.avatar_url) : null,
492
- avatarStorageKey: row.avatar_storage_key ? String(row.avatar_storage_key) : null,
493
- status,
494
- verificationTokenHash: row.verification_token_hash ? String(row.verification_token_hash) : null,
495
- verificationSentAt: row.verification_sent_at ? String(row.verification_sent_at) : null,
496
- verifiedAt: row.verified_at ? String(row.verified_at) : null,
497
- createdAt: String(row.created_at),
498
- updatedAt: String(row.updated_at)
499
- };
500
- }
501
- function mapArtifact(row) {
502
- return {
503
- id: String(row.id),
504
- jobId: String(row.job_id),
505
- customerId: String(row.customer_id),
506
- templateId: String(row.template_id),
507
- kind: String(row.kind),
508
- storageKey: String(row.storage_key),
509
- publicUrl: row.public_url ? String(row.public_url) : null,
510
- metadata: parseJson(row.metadata_json, {}),
511
- createdAt: String(row.created_at)
512
- };
513
- }
514
- function mapReadyPost(row) {
515
- return {
516
- id: String(row.id),
517
- customerId: String(row.customer_id),
518
- status: row.status,
519
- tracer: row.tracer ? String(row.tracer) : null,
520
- title: row.title ? String(row.title) : null,
521
- caption: String(row.content),
522
- pinnedComment: row.pinned_comment ? String(row.pinned_comment) : null,
523
- mediaAssets: parseJson(row.media_assets_json, []),
524
- sharePasswordHash: row.share_password_hash ? String(row.share_password_hash) : null,
525
- zipStorageKey: row.zip_storage_key ? String(row.zip_storage_key) : null,
526
- zipPublicUrl: row.zip_public_url ? String(row.zip_public_url) : null,
527
- archivedAt: row.archived_at ? String(row.archived_at) : null,
528
- deletedAt: row.deleted_at ? String(row.deleted_at) : null,
529
- createdAt: String(row.created_at),
530
- updatedAt: String(row.updated_at)
531
- };
532
- }
533
- function mapPostSchedule(row) {
534
- return {
535
- id: String(row.id),
536
- customerId: String(row.customer_id),
537
- readyPostId: row.ready_post_id ? String(row.ready_post_id) : null,
538
- managedBy: row.managed_by,
539
- provider: row.provider,
540
- destinationType: row.destination_type,
541
- destinationId: String(row.destination_id),
542
- scheduledAt: String(row.scheduled_at),
543
- timezone: row.timezone ? String(row.timezone) : null,
544
- status: row.status,
545
- providerStatus: row.provider_status ? String(row.provider_status) : null,
546
- providerScheduleId: row.provider_schedule_id ? String(row.provider_schedule_id) : null,
547
- title: row.title ? String(row.title) : null,
548
- summary: row.summary ? String(row.summary) : null,
549
- url: row.url ? String(row.url) : null,
550
- additionalNotes: row.additional_notes ? String(row.additional_notes) : null,
551
- providerPayload: parseJson(row.provider_payload_json, {}),
552
- providerResponse: parseJson(row.provider_response_json, {}),
553
- lastSyncedAt: row.last_synced_at ? String(row.last_synced_at) : null,
554
- lastError: row.last_error ? String(row.last_error) : null,
555
- cancelledAt: row.cancelled_at ? String(row.cancelled_at) : null,
556
- sentAt: row.sent_at ? String(row.sent_at) : null,
557
- createdAt: String(row.created_at),
558
- updatedAt: String(row.updated_at)
559
- };
560
- }
561
- function mapChatThread(row, tracers, messages = []) {
562
- return {
563
- id: String(row.id),
564
- customerId: String(row.customer_id),
565
- templateId: String(row.template_id),
566
- title: String(row.title),
567
- createdAt: String(row.created_at),
568
- updatedAt: String(row.updated_at),
569
- lastMessageAt: row.last_message_at ? String(row.last_message_at) : null,
570
- messageCount: Number(row.message_count ?? 0),
571
- tracers,
572
- hiddenFromView: Boolean(row.hidden_from_view),
573
- archivedAt: row.archived_at ? String(row.archived_at) : null,
574
- storagePrefix: String(row.storage_prefix),
575
- messages
576
- };
577
- }
578
- export const database = {
579
- raw: db,
580
- upsertCustomer(input) {
581
- const timestamp = nowIso();
582
- const current = db.prepare(`select * from customers where id = ?`).get(input.id);
583
- db.prepare(`
584
- insert into customers (
585
- id, email, name, default_webhook_url, is_developer, is_paid_plan, about, groupchat_url, flockposter_api_key, password_hash, created_at, updated_at
586
- , plan_tier
587
- )
588
- values (
589
- @id, @email, @name, @default_webhook_url, @is_developer, @is_paid_plan, @about, @groupchat_url, @flockposter_api_key, @password_hash, @created_at, @updated_at
590
- , @plan_tier
591
- )
592
- on conflict(id) do update set
593
- email = excluded.email,
594
- name = excluded.name,
595
- default_webhook_url = excluded.default_webhook_url,
596
- is_developer = excluded.is_developer,
597
- is_paid_plan = excluded.is_paid_plan,
598
- plan_tier = excluded.plan_tier,
599
- about = excluded.about,
600
- groupchat_url = excluded.groupchat_url,
601
- flockposter_api_key = excluded.flockposter_api_key,
602
- password_hash = excluded.password_hash,
603
- updated_at = excluded.updated_at
604
- `).run({
605
- id: input.id,
606
- email: input.email,
607
- name: input.name ?? null,
608
- default_webhook_url: input.defaultWebhookUrl ?? null,
609
- is_developer: input.isDeveloper === undefined ? Number(current?.is_developer ?? 0) : (input.isDeveloper ? 1 : 0),
610
- is_paid_plan: input.isPaidPlan === undefined ? Number(current?.is_paid_plan ?? 0) : (input.isPaidPlan ? 1 : 0),
611
- plan_tier: input.planTier === undefined ? String(current?.plan_tier ?? "starter") : input.planTier,
612
- about: input.about === undefined ? (current?.about ?? null) : input.about,
613
- groupchat_url: input.groupchatUrl === undefined ? (current?.groupchat_url ?? null) : input.groupchatUrl,
614
- flockposter_api_key: input.flockposterApiKey === undefined ? (current?.flockposter_api_key ?? null) : input.flockposterApiKey,
615
- password_hash: input.passwordHash === undefined ? (current?.password_hash ?? null) : input.passwordHash,
616
- created_at: timestamp,
617
- updated_at: timestamp
618
- });
619
- return this.getCustomerById(input.id);
620
- },
621
- getCustomerById(id) {
622
- const row = db.prepare(`select * from customers where id = ?`).get(id);
623
- if (!row) {
624
- return null;
625
- }
626
- return {
627
- id: String(row.id),
628
- email: String(row.email),
629
- name: row.name ? String(row.name) : null,
630
- defaultWebhookUrl: row.default_webhook_url ? String(row.default_webhook_url) : null,
631
- isDeveloper: Boolean(row.is_developer),
632
- isPaidPlan: Boolean(row.is_paid_plan),
633
- planTier: normalizeCustomerPlanTier(row.plan_tier),
634
- about: row.about ? String(row.about) : null,
635
- groupchatUrl: row.groupchat_url ? String(row.groupchat_url) : null,
636
- flockposterApiKey: row.flockposter_api_key ? String(row.flockposter_api_key) : null,
637
- createdAt: String(row.created_at),
638
- updatedAt: String(row.updated_at)
639
- };
640
- },
641
- getCustomerByEmail(email) {
642
- const row = db.prepare(`select * from customers where lower(email) = lower(?)`).get(email);
643
- return row ? this.getCustomerById(String(row.id)) : null;
644
- },
645
- getFirstPaidCustomer() {
646
- const row = db.prepare(`
647
- select id
648
- from customers
649
- where is_paid_plan = 1
650
- order by created_at asc
651
- limit 1
652
- `).get();
653
- return row?.id ? this.getCustomerById(String(row.id)) : null;
654
- },
655
- listPaidCustomers() {
656
- const rows = db.prepare(`
657
- select id
658
- from customers
659
- where is_paid_plan = 1
660
- order by created_at asc
661
- `).all();
662
- return rows
663
- .map((row) => this.getCustomerById(String(row.id)))
664
- .filter((customer) => Boolean(customer));
665
- },
666
- getCustomerAuthByEmail(email) {
667
- return db.prepare(`
668
- select id, email, name, default_webhook_url, is_developer, is_paid_plan, password_hash, plan_tier, created_at, updated_at
669
- , about, groupchat_url, flockposter_api_key
670
- from customers
671
- where lower(email) = lower(?)
672
- limit 1
673
- `).get(email);
674
- },
675
- updateCustomerPassword(customerId, passwordHash) {
676
- db.prepare(`
677
- update customers
678
- set password_hash = ?, updated_at = ?
679
- where id = ?
680
- `).run(passwordHash, nowIso(), customerId);
681
- },
682
- updateCustomerPaidPlan(customerId, isPaidPlan, planTier) {
683
- const existing = this.getCustomerById(customerId);
684
- db.prepare(`
685
- update customers
686
- set is_paid_plan = ?, plan_tier = ?, updated_at = ?
687
- where id = ?
688
- `).run(isPaidPlan ? 1 : 0, planTier ?? existing?.planTier ?? "starter", nowIso(), customerId);
689
- },
690
- hasCustomerCreditGrant(customerId, grantType) {
691
- const row = db.prepare(`
692
- select 1
693
- from customer_credit_grants
694
- where customer_id = ? and grant_type = ?
695
- limit 1
696
- `).get(customerId, grantType);
697
- return Boolean(row);
698
- },
699
- insertCustomerCreditGrant(record) {
700
- const createdAt = nowIso();
701
- db.prepare(`
702
- insert into customer_credit_grants (id, customer_id, grant_type, amount_usd, metadata_json, created_at)
703
- values (@id, @customer_id, @grant_type, @amount_usd, @metadata_json, @created_at)
704
- `).run({
705
- id: record.id,
706
- customer_id: record.customerId,
707
- grant_type: record.grantType,
708
- amount_usd: record.amountUsd,
709
- metadata_json: stringifyJson(record.metadata ?? {}),
710
- created_at: createdAt
711
- });
712
- },
713
- setCustomerAccessByEmail(input) {
714
- const normalizedEmail = input.email.trim().toLowerCase();
715
- const existing = this.getCustomerByEmail(normalizedEmail);
716
- return this.upsertCustomer({
717
- id: existing?.id ?? createId("cus"),
718
- email: normalizedEmail,
719
- name: input.name === undefined ? (existing?.name ?? null) : input.name,
720
- defaultWebhookUrl: existing?.defaultWebhookUrl ?? null,
721
- isDeveloper: input.isDeveloper === undefined ? (existing?.isDeveloper ?? false) : input.isDeveloper,
722
- isPaidPlan: input.isPaidPlan === undefined ? (existing?.isPaidPlan ?? false) : input.isPaidPlan,
723
- planTier: input.planTier === undefined ? (existing?.planTier ?? "starter") : input.planTier,
724
- about: existing?.about ?? null,
725
- groupchatUrl: existing?.groupchatUrl ?? null,
726
- flockposterApiKey: existing?.flockposterApiKey ?? null
727
- });
728
- },
729
- updateCustomerProfile(input) {
730
- const existing = this.getCustomerById(input.customerId);
731
- db.prepare(`
732
- update customers
733
- set name = ?, about = ?, groupchat_url = ?, flockposter_api_key = ?, updated_at = ?
734
- where id = ?
735
- `).run(input.name === undefined ? (existing?.name ?? null) : input.name, input.about === undefined ? (existing?.about ?? null) : input.about, input.groupchatUrl === undefined ? (existing?.groupchatUrl ?? null) : input.groupchatUrl, input.flockposterApiKey === undefined ? (existing?.flockposterApiKey ?? null) : input.flockposterApiKey, nowIso(), input.customerId);
736
- },
737
- listProviderKeys(customerId) {
738
- return db.prepare(`
739
- select id, provider, label, status, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
740
- from customer_provider_keys
741
- where customer_id = ?
742
- order by provider asc, created_at desc
743
- `).all(customerId);
744
- },
745
- listProviderKeysWithSecrets(customerId) {
746
- return db.prepare(`
747
- select id, provider, label, encrypted_secret as secret, status, last_used_at, cooldown_until, disabled_reason, created_at, updated_at
748
- from customer_provider_keys
749
- where customer_id = ?
750
- order by provider asc, created_at desc
751
- `).all(customerId);
752
- },
753
- deleteProviderKey(customerId, keyId) {
754
- db.prepare(`delete from customer_provider_keys where id = ? and customer_id = ?`).run(keyId, customerId);
755
- },
756
- createEmailChannel(record) {
757
- const timestamp = nowIso();
758
- db.prepare(`
759
- insert into email_channels (
760
- id, customer_id, title, email, note, avatar_url, avatar_storage_key, status, verification_token_hash,
761
- verification_sent_at, verified_at, created_at, updated_at
762
- ) values (
763
- @id, @customer_id, @title, @email, @note, @avatar_url, @avatar_storage_key, @status, @verification_token_hash,
764
- @verification_sent_at, null, @created_at, @updated_at
765
- )
766
- `).run({
767
- id: record.id,
768
- customer_id: record.customerId,
769
- title: record.title,
770
- email: record.email,
771
- note: record.note ?? null,
772
- avatar_url: record.avatarUrl ?? null,
773
- avatar_storage_key: record.avatarStorageKey ?? null,
774
- status: record.status,
775
- verification_token_hash: record.verificationTokenHash ?? null,
776
- verification_sent_at: record.verificationSentAt ?? null,
777
- created_at: timestamp,
778
- updated_at: timestamp
779
- });
780
- return this.getEmailChannel(record.customerId, record.id);
781
- },
782
- listEmailChannels(customerId) {
783
- const rows = db.prepare(`
784
- select *
785
- from email_channels
786
- where customer_id = ?
787
- order by created_at desc
788
- `).all(customerId);
789
- return rows.map(mapEmailChannel);
790
- },
791
- getEmailChannel(customerId, channelId) {
792
- const row = db.prepare(`
793
- select *
794
- from email_channels
795
- where customer_id = ? and id = ?
796
- limit 1
797
- `).get(customerId, channelId);
798
- return row ? mapEmailChannel(row) : null;
799
- },
800
- getEmailChannelByEmail(customerId, email) {
801
- const row = db.prepare(`
802
- select *
803
- from email_channels
804
- where customer_id = ? and lower(email) = lower(?)
805
- limit 1
806
- `).get(customerId, email);
807
- return row ? mapEmailChannel(row) : null;
808
- },
809
- findEmailChannelByVerificationHash(tokenHash) {
810
- const row = db.prepare(`
811
- select *
812
- from email_channels
813
- where verification_token_hash = ?
814
- limit 1
815
- `).get(tokenHash);
816
- return row ? mapEmailChannel(row) : null;
817
- },
818
- updateEmailChannel(input) {
819
- db.prepare(`
820
- update email_channels
821
- set title = ?, note = ?, updated_at = ?
822
- where customer_id = ? and id = ?
823
- `).run(input.title, input.note ?? null, nowIso(), input.customerId, input.channelId);
824
- return this.getEmailChannel(input.customerId, input.channelId);
825
- },
826
- updateEmailChannelAvatar(input) {
827
- db.prepare(`
828
- update email_channels
829
- set avatar_url = ?, avatar_storage_key = ?, updated_at = ?
830
- where customer_id = ? and id = ?
831
- `).run(input.avatarUrl, input.avatarStorageKey, nowIso(), input.customerId, input.channelId);
832
- return this.getEmailChannel(input.customerId, input.channelId);
833
- },
834
- updateEmailChannelVerification(input) {
835
- db.prepare(`
836
- update email_channels
837
- set status = 'unverified',
838
- verification_token_hash = ?,
839
- verification_sent_at = ?,
840
- verified_at = null,
841
- updated_at = ?
842
- where customer_id = ? and id = ?
843
- `).run(input.tokenHash, input.sentAt, nowIso(), input.customerId, input.channelId);
844
- return this.getEmailChannel(input.customerId, input.channelId);
845
- },
846
- verifyEmailChannelByTokenHash(tokenHash) {
847
- const existing = this.findEmailChannelByVerificationHash(tokenHash);
848
- if (!existing) {
849
- return null;
850
- }
851
- const timestamp = nowIso();
852
- db.prepare(`
853
- update email_channels
854
- set status = 'verified',
855
- verification_token_hash = null,
856
- verified_at = ?,
857
- updated_at = ?
858
- where id = ?
859
- `).run(timestamp, timestamp, existing.id);
860
- return this.getEmailChannel(existing.customerId, existing.id);
861
- },
862
- deleteEmailChannel(customerId, channelId) {
863
- db.prepare(`delete from email_channels where customer_id = ? and id = ?`).run(customerId, channelId);
864
- },
865
- insertOtpChallenge(record) {
866
- db.prepare(`
867
- insert into otp_challenges (id, email, code_hash, expires_at, created_at)
868
- values (@id, @email, @code_hash, @expires_at, @created_at)
869
- `).run({
870
- id: record.id,
871
- email: record.email,
872
- code_hash: record.codeHash,
873
- expires_at: record.expiresAt,
874
- created_at: nowIso()
875
- });
876
- },
877
- findLatestOtpChallenge(email) {
878
- return db.prepare(`
879
- select * from otp_challenges
880
- where lower(email) = lower(?)
881
- order by created_at desc
882
- limit 1
883
- `).get(email);
884
- },
885
- consumeOtpChallenge(id) {
886
- db.prepare(`update otp_challenges set consumed_at = ? where id = ?`).run(nowIso(), id);
887
- },
888
- insertApiKey(record) {
889
- const timestamp = nowIso();
890
- db.prepare(`
891
- insert into api_keys (id, customer_id, key_hash, raw_value, label, status, created_at, updated_at)
892
- values (@id, @customer_id, @key_hash, @raw_value, @label, 'active', @created_at, @updated_at)
893
- `).run({
894
- id: record.id,
895
- customer_id: record.customerId,
896
- key_hash: record.keyHash,
897
- raw_value: record.rawValue ?? null,
898
- label: record.label,
899
- created_at: timestamp,
900
- updated_at: timestamp
901
- });
902
- },
903
- getLatestApiKeyForCustomer(customerId) {
904
- return db.prepare(`
905
- select id, raw_value, label, last_used_at, created_at, updated_at
906
- from api_keys
907
- where customer_id = ? and status = 'active'
908
- order by case when raw_value is null or trim(raw_value) = '' then 1 else 0 end asc, created_at desc
909
- limit 1
910
- `).get(customerId);
911
- },
912
- findApiKeyByHash(keyHash) {
913
- return db.prepare(`
914
- select a.*, c.email, c.name, c.default_webhook_url, c.is_developer, c.is_paid_plan, c.plan_tier, c.about, c.groupchat_url, c.flockposter_api_key, c.created_at, c.updated_at
915
- from api_keys a
916
- join customers c on c.id = a.customer_id
917
- where a.key_hash = ? and a.status = 'active'
918
- `).get(keyHash);
919
- },
920
- touchApiKey(id) {
921
- db.prepare(`update api_keys set last_used_at = ?, updated_at = ? where id = ?`).run(nowIso(), nowIso(), id);
922
- },
923
- upsertTemplateConfig(record) {
924
- const timestamp = nowIso();
925
- db.prepare(`
926
- insert into template_configs (id, customer_id, template_id, config_json, created_at, updated_at)
927
- values (@id, @customer_id, @template_id, @config_json, @created_at, @updated_at)
928
- on conflict(customer_id, template_id) do update set
929
- config_json = excluded.config_json,
930
- updated_at = excluded.updated_at
931
- `).run({
932
- id: record.id,
933
- customer_id: record.customerId,
934
- template_id: record.templateId,
935
- config_json: stringifyJson(record.config),
936
- created_at: timestamp,
937
- updated_at: timestamp
938
- });
939
- },
940
- getTemplateConfig(customerId, templateId) {
941
- const row = db.prepare(`
942
- select config_json from template_configs
943
- where customer_id = ? and template_id = ?
944
- `).get(customerId, templateId);
945
- return parseJson(row?.config_json, {});
946
- },
947
- createJob(record) {
948
- const timestamp = nowIso();
949
- db.prepare(`
950
- insert into jobs (
951
- id, template_id, operation_name, workflow_name, tracer, status, customer_id,
952
- payload_json, result_json, error_json, progress, webhook_url, parent_job_id,
953
- priority, attempt_count, max_attempts, run_after, created_at, updated_at, started_at, completed_at
954
- )
955
- values (
956
- @id, @template_id, @operation_name, @workflow_name, @tracer, @status, @customer_id,
957
- @payload_json, @result_json, @error_json, @progress, @webhook_url, @parent_job_id,
958
- @priority, @attempt_count, @max_attempts, @run_after, @created_at, @updated_at, @started_at, @completed_at
959
- )
960
- `).run({
961
- id: record.id,
962
- template_id: record.templateId,
963
- operation_name: record.operationName,
964
- workflow_name: record.workflowName,
965
- tracer: record.tracer,
966
- status: record.status,
967
- customer_id: record.customerId,
968
- payload_json: stringifyJson(record.payload),
969
- result_json: stringifyJson(record.result ?? null),
970
- error_json: stringifyJson(record.error ?? null),
971
- progress: record.progress,
972
- webhook_url: record.webhookUrl,
973
- parent_job_id: record.parentJobId,
974
- priority: record.priority,
975
- attempt_count: record.attemptCount,
976
- max_attempts: record.maxAttempts,
977
- run_after: record.runAfter,
978
- created_at: timestamp,
979
- updated_at: timestamp,
980
- started_at: record.startedAt ?? null,
981
- completed_at: record.completedAt ?? null
982
- });
983
- return this.getJob(record.id);
984
- },
985
- getJob(id) {
986
- const row = db.prepare(`select * from jobs where id = ?`).get(id);
987
- return row ? mapJob(row) : null;
988
- },
989
- getJobsByIds(ids) {
990
- const uniqueIds = [...new Set(ids.map((id) => String(id).trim()).filter(Boolean))];
991
- if (!uniqueIds.length) {
992
- return [];
993
- }
994
- const rows = db.prepare(`
995
- select * from jobs
996
- where id in (${uniqueIds.map(() => "?").join(", ")})
997
- `).all(...uniqueIds);
998
- return rows.map(mapJob);
999
- },
1000
- countJobsByStatuses(input) {
1001
- const statuses = input.statuses.length ? input.statuses : ["queued"];
1002
- const filters = [`status in (${statuses.map(() => "?").join(", ")})`];
1003
- const values = [...statuses];
1004
- if (input.customerId) {
1005
- filters.push("customer_id = ?");
1006
- values.push(input.customerId);
1007
- }
1008
- const row = db.prepare(`
1009
- select count(*) as count
1010
- from jobs
1011
- where ${filters.join(" and ")}
1012
- `).get(...values);
1013
- return Number(row.count);
1014
- },
1015
- listRunnableJobs(limit) {
1016
- const rows = db.prepare(`
1017
- select * from jobs
1018
- where status = 'queued' and run_after <= ?
1019
- order by priority asc, created_at asc
1020
- limit ?
1021
- `).all(nowIso(), limit);
1022
- return rows.map(mapJob);
1023
- },
1024
- listAvailableProviderKeyCounts() {
1025
- const rows = db.prepare(`
1026
- select
1027
- k.customer_id,
1028
- k.provider,
1029
- count(*) as available_count
1030
- from customer_provider_keys k
1031
- left join provider_key_leases l
1032
- on l.key_id = k.id
1033
- and l.expires_at > ?
1034
- where k.status = 'active'
1035
- and (k.cooldown_until is null or k.cooldown_until <= ?)
1036
- and l.key_id is null
1037
- group by k.customer_id, k.provider
1038
- `).all(nowIso(), nowIso());
1039
- return rows.map((row) => ({
1040
- customerId: row.customer_id,
1041
- provider: row.provider,
1042
- availableCount: Number(row.available_count),
1043
- }));
1044
- },
1045
- markJobRunning(id) {
1046
- const timestamp = nowIso();
1047
- db.prepare(`
1048
- update jobs
1049
- set status = 'running',
1050
- started_at = coalesce(started_at, ?),
1051
- error_json = null,
1052
- completed_at = null,
1053
- updated_at = ?,
1054
- attempt_count = attempt_count + 1
1055
- where id = ?
1056
- `).run(timestamp, timestamp, id);
1057
- },
1058
- updateJobStatus(input) {
1059
- const current = this.getJob(input.id);
1060
- db.prepare(`
1061
- update jobs
1062
- set status = @status,
1063
- progress = @progress,
1064
- result_json = @result_json,
1065
- error_json = @error_json,
1066
- run_after = @run_after,
1067
- completed_at = @completed_at,
1068
- updated_at = @updated_at
1069
- where id = @id
1070
- `).run({
1071
- id: input.id,
1072
- status: input.status,
1073
- progress: input.progress ?? current?.progress ?? 0,
1074
- result_json: input.result === undefined ? stringifyJson(current?.result ?? null) : stringifyJson(input.result),
1075
- error_json: input.error === undefined ? stringifyJson(current?.error ?? null) : stringifyJson(input.error),
1076
- run_after: input.runAfter ?? current?.runAfter ?? nowIso(),
1077
- completed_at: input.completedAt ?? current?.completedAt ?? null,
1078
- updated_at: nowIso()
1079
- });
1080
- },
1081
- listJobsForCustomer(input) {
1082
- const filters = ["customer_id = ?"];
1083
- const values = [input.customerId];
1084
- if (input.templateId) {
1085
- filters.push("template_id = ?");
1086
- values.push(input.templateId);
1087
- }
1088
- if (input.tracer) {
1089
- filters.push("tracer = ?");
1090
- values.push(input.tracer);
1091
- }
1092
- if (input.startTime) {
1093
- filters.push("created_at >= ?");
1094
- values.push(input.startTime);
1095
- }
1096
- if (input.endTime) {
1097
- filters.push("created_at <= ?");
1098
- values.push(input.endTime);
1099
- }
1100
- if (input.cursor) {
1101
- filters.push("(created_at < ? or (created_at = ? and id < ?))");
1102
- values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
1103
- }
1104
- values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
1105
- const rows = db.prepare(`
1106
- select * from jobs
1107
- where ${filters.join(" and ")}
1108
- order by created_at desc, id desc
1109
- limit ?
1110
- `).all(...values);
1111
- return rows.map(mapJob);
1112
- },
1113
- renameJobsTracer(input) {
1114
- const fromTracer = input.fromTracer.trim();
1115
- const toTracer = input.toTracer.trim();
1116
- if (!fromTracer || !toTracer || fromTracer === toTracer) {
1117
- return 0;
1118
- }
1119
- const filters = ["customer_id = @customer_id", "tracer = @from_tracer"];
1120
- if (input.templateId) {
1121
- filters.push("template_id = @template_id");
1122
- }
1123
- const result = db.prepare(`
1124
- update jobs
1125
- set tracer = @to_tracer,
1126
- updated_at = @updated_at
1127
- where ${filters.join(" and ")}
1128
- `).run({
1129
- customer_id: input.customerId,
1130
- template_id: input.templateId ?? null,
1131
- from_tracer: fromTracer,
1132
- to_tracer: toTracer,
1133
- updated_at: nowIso()
1134
- });
1135
- return Number(result.changes || 0);
1136
- },
1137
- insertArtifact(record) {
1138
- db.prepare(`
1139
- insert into artifacts (id, job_id, customer_id, template_id, kind, storage_key, public_url, metadata_json, created_at)
1140
- values (@id, @job_id, @customer_id, @template_id, @kind, @storage_key, @public_url, @metadata_json, @created_at)
1141
- `).run({
1142
- id: record.id,
1143
- job_id: record.jobId,
1144
- customer_id: record.customerId,
1145
- template_id: record.templateId,
1146
- kind: record.kind,
1147
- storage_key: record.storageKey,
1148
- public_url: record.publicUrl ?? null,
1149
- metadata_json: stringifyJson(record.metadata ?? {}),
1150
- created_at: nowIso()
1151
- });
1152
- },
1153
- listArtifactsForJob(input) {
1154
- const filters = ["job_id = ?", "customer_id = ?"];
1155
- const values = [input.jobId, input.customerId];
1156
- if (input.templateId) {
1157
- filters.push("template_id = ?");
1158
- values.push(input.templateId);
1159
- }
1160
- if (input.cursor) {
1161
- filters.push("(created_at < ? or (created_at = ? and id < ?))");
1162
- values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
1163
- }
1164
- values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
1165
- const rows = db.prepare(`
1166
- select *
1167
- from artifacts
1168
- where ${filters.join(" and ")}
1169
- order by created_at desc, id desc
1170
- limit ?
1171
- `).all(...values);
1172
- return rows.map(mapArtifact);
1173
- },
1174
- queueWebhookDelivery(record) {
1175
- const timestamp = nowIso();
1176
- db.prepare(`
1177
- insert into webhook_deliveries (
1178
- id, job_id, customer_id, event_type, destination_url, payload_json, status,
1179
- attempt_count, next_attempt_at, created_at, updated_at
1180
- ) values (
1181
- @id, @job_id, @customer_id, @event_type, @destination_url, @payload_json, 'pending',
1182
- 0, @next_attempt_at, @created_at, @updated_at
1183
- )
1184
- `).run({
1185
- id: record.id,
1186
- job_id: record.jobId,
1187
- customer_id: record.customerId,
1188
- event_type: record.eventType,
1189
- destination_url: record.destinationUrl,
1190
- payload_json: stringifyJson(record.payload),
1191
- next_attempt_at: timestamp,
1192
- created_at: timestamp,
1193
- updated_at: timestamp
1194
- });
1195
- },
1196
- listPendingWebhookDeliveries(limit) {
1197
- return db.prepare(`
1198
- select * from webhook_deliveries
1199
- where status in ('pending', 'retrying') and next_attempt_at <= ?
1200
- order by next_attempt_at asc
1201
- limit ?
1202
- `).all(nowIso(), limit);
1203
- },
1204
- markWebhookDelivery(record) {
1205
- db.prepare(`
1206
- update webhook_deliveries
1207
- set status = @status,
1208
- attempt_count = @attempt_count,
1209
- next_attempt_at = coalesce(@next_attempt_at, next_attempt_at),
1210
- last_error = @last_error,
1211
- updated_at = @updated_at
1212
- where id = @id
1213
- `).run({
1214
- id: record.id,
1215
- status: record.status,
1216
- attempt_count: record.attemptCount,
1217
- next_attempt_at: record.nextAttemptAt ?? null,
1218
- last_error: record.lastError ?? null,
1219
- updated_at: nowIso()
1220
- });
1221
- },
1222
- acquireProviderKeyLease(input) {
1223
- const transaction = db.transaction(() => {
1224
- db.prepare(`delete from provider_key_leases where expires_at <= ?`).run(nowIso());
1225
- db.prepare(`delete from provider_key_waiters where expires_at <= ?`).run(nowIso());
1226
- db.prepare(`
1227
- insert into provider_key_waiters (id, customer_id, provider, worker_id, owner_id, priority, created_at, expires_at)
1228
- values (?, ?, ?, ?, ?, ?, coalesce((select created_at from provider_key_waiters where id = ?), ?), ?)
1229
- on conflict(id) do update set
1230
- worker_id = excluded.worker_id,
1231
- owner_id = excluded.owner_id,
1232
- priority = excluded.priority,
1233
- expires_at = excluded.expires_at
1234
- `).run(input.waiterId, input.customerId, input.provider, input.workerId, input.jobId, input.priority, input.waiterId, nowIso(), input.waiterExpiresAt);
1235
- const nextWaiter = db.prepare(`
1236
- select id
1237
- from provider_key_waiters
1238
- where customer_id = ?
1239
- and provider = ?
1240
- and expires_at > ?
1241
- order by priority asc, created_at asc
1242
- limit 1
1243
- `).get(input.customerId, input.provider, nowIso());
1244
- if (!nextWaiter || nextWaiter.id !== input.waiterId) {
1245
- return null;
1246
- }
1247
- const row = db.prepare(`
1248
- select k.id, k.encrypted_secret
1249
- from customer_provider_keys k
1250
- left join provider_key_leases l on l.key_id = k.id and l.expires_at > ?
1251
- where k.customer_id = ?
1252
- and k.provider = ?
1253
- and k.status = 'active'
1254
- and (k.cooldown_until is null or k.cooldown_until <= ?)
1255
- and l.key_id is null
1256
- order by case when k.last_used_at is null then 0 else 1 end asc, k.last_used_at asc
1257
- limit 1
1258
- `).get(nowIso(), input.customerId, input.provider, nowIso());
1259
- if (!row) {
1260
- return null;
1261
- }
1262
- db.prepare(`
1263
- insert into provider_key_leases (key_id, lease_token, worker_id, job_id, leased_at, expires_at)
1264
- values (?, ?, ?, ?, ?, ?)
1265
- `).run(row.id, input.leaseToken, input.workerId, input.jobId, nowIso(), input.expiresAt);
1266
- db.prepare(`delete from provider_key_waiters where id = ?`).run(input.waiterId);
1267
- return { keyId: row.id, encryptedSecret: row.encrypted_secret };
1268
- });
1269
- return transaction();
1270
- },
1271
- deleteProviderKeyWaiter(waiterId) {
1272
- db.prepare(`delete from provider_key_waiters where id = ?`).run(waiterId);
1273
- },
1274
- releaseProviderKeyLease(input) {
1275
- db.prepare(`delete from provider_key_leases where key_id = ? and lease_token = ?`).run(input.keyId, input.leaseToken);
1276
- },
1277
- recordProviderKeyUsage(input) {
1278
- db.prepare(`
1279
- insert into provider_key_usage_events (
1280
- id, key_id, job_id, provider, model, event_type, input_tokens, output_tokens, cost_usd, metadata, created_at
1281
- ) values (
1282
- @id, @key_id, @job_id, @provider, @model, @event_type, @input_tokens, @output_tokens, @cost_usd, @metadata, @created_at
1283
- )
1284
- `).run({
1285
- id: input.id,
1286
- key_id: input.keyId,
1287
- job_id: input.jobId,
1288
- provider: input.provider,
1289
- model: input.model ?? null,
1290
- event_type: input.eventType,
1291
- input_tokens: input.inputTokens ?? null,
1292
- output_tokens: input.outputTokens ?? null,
1293
- cost_usd: input.costUsd ?? null,
1294
- metadata: stringifyJson(input.metadata ?? {}),
1295
- created_at: nowIso()
1296
- });
1297
- },
1298
- touchProviderKey(keyId) {
1299
- db.prepare(`update customer_provider_keys set last_used_at = ?, updated_at = ? where id = ?`).run(nowIso(), nowIso(), keyId);
1300
- },
1301
- setProviderKeyCooldown(keyId, cooldownUntil, status = "active", disabledReason = null) {
1302
- db.prepare(`
1303
- update customer_provider_keys
1304
- set cooldown_until = ?, status = ?, disabled_reason = ?, updated_at = ?
1305
- where id = ?
1306
- `).run(cooldownUntil, status, disabledReason, nowIso(), keyId);
1307
- },
1308
- createProviderKey(record) {
1309
- const timestamp = nowIso();
1310
- db.prepare(`
1311
- insert into customer_provider_keys (
1312
- id, customer_id, provider, label, encrypted_secret, status, created_at, updated_at
1313
- ) values (
1314
- @id, @customer_id, @provider, @label, @encrypted_secret, 'active', @created_at, @updated_at
1315
- )
1316
- `).run({
1317
- id: record.id,
1318
- customer_id: record.customerId,
1319
- provider: record.provider,
1320
- label: record.label,
1321
- encrypted_secret: record.encryptedSecret,
1322
- created_at: timestamp,
1323
- updated_at: timestamp
1324
- });
1325
- },
1326
- createUserAttachment(record) {
1327
- db.prepare(`
1328
- insert into user_attachments (
1329
- id, customer_id, file_name, content_type, size_bytes, storage_key, public_url, created_at
1330
- ) values (
1331
- @id, @customer_id, @file_name, @content_type, @size_bytes, @storage_key, @public_url, @created_at
1332
- )
1333
- `).run({
1334
- id: record.id,
1335
- customer_id: record.customerId,
1336
- file_name: record.fileName,
1337
- content_type: record.contentType,
1338
- size_bytes: record.sizeBytes,
1339
- storage_key: record.storageKey,
1340
- public_url: record.publicUrl ?? null,
1341
- created_at: nowIso()
1342
- });
1343
- },
1344
- listUserAttachments(customerId) {
1345
- const rows = db.prepare(`
1346
- select *
1347
- from user_attachments
1348
- where customer_id = ?
1349
- order by created_at desc
1350
- `).all(customerId);
1351
- return rows.map(mapUserAttachment);
1352
- },
1353
- getUserAttachment(customerId, attachmentId) {
1354
- const row = db.prepare(`
1355
- select *
1356
- from user_attachments
1357
- where customer_id = ? and id = ?
1358
- limit 1
1359
- `).get(customerId, attachmentId);
1360
- return row ? mapUserAttachment(row) : null;
1361
- },
1362
- deleteUserAttachment(customerId, attachmentId) {
1363
- db.prepare(`delete from user_attachments where customer_id = ? and id = ?`).run(customerId, attachmentId);
1364
- },
1365
- createReadyPost(record) {
1366
- const timestamp = nowIso();
1367
- db.prepare(`
1368
- insert into ready_posts (
1369
- id, customer_id, status, tracer, title, content, pinned_comment, scheduled_at, timezone, post_type,
1370
- integration_ids_json, platform_targets_json, media_assets_json, renditions_json,
1371
- flockposter_payload_json, field_values_json, share_password_hash, zip_storage_key,
1372
- zip_public_url, archived_at, deleted_at, created_at, updated_at
1373
- ) values (
1374
- @id, @customer_id, @status, @tracer, @title, @content, @pinned_comment, @scheduled_at, @timezone, @post_type,
1375
- @integration_ids_json, @platform_targets_json, @media_assets_json, @renditions_json,
1376
- @flockposter_payload_json, @field_values_json, @share_password_hash, @zip_storage_key,
1377
- @zip_public_url, @archived_at, @deleted_at, @created_at, @updated_at
1378
- )
1379
- `).run({
1380
- id: record.id,
1381
- customer_id: record.customerId,
1382
- status: record.status,
1383
- tracer: record.tracer,
1384
- title: record.title,
1385
- content: record.caption,
1386
- pinned_comment: record.pinnedComment,
1387
- scheduled_at: null,
1388
- timezone: null,
1389
- post_type: "draft",
1390
- integration_ids_json: stringifyJson([]),
1391
- platform_targets_json: stringifyJson([]),
1392
- media_assets_json: stringifyJson(record.mediaAssets),
1393
- renditions_json: stringifyJson([]),
1394
- flockposter_payload_json: stringifyJson({}),
1395
- field_values_json: stringifyJson({}),
1396
- share_password_hash: record.sharePasswordHash,
1397
- zip_storage_key: record.zipStorageKey ?? null,
1398
- zip_public_url: record.zipPublicUrl ?? null,
1399
- archived_at: record.archivedAt ?? null,
1400
- deleted_at: record.deletedAt ?? null,
1401
- created_at: timestamp,
1402
- updated_at: timestamp
1403
- });
1404
- return this.getReadyPost(record.id);
1405
- },
1406
- getReadyPost(id) {
1407
- const row = db.prepare(`select * from ready_posts where id = ? limit 1`).get(id);
1408
- return row ? mapReadyPost(row) : null;
1409
- },
1410
- listReadyPosts(customerId, input) {
1411
- const filters = ["customer_id = ?", "status != 'deleted'"];
1412
- const values = [customerId];
1413
- if (input?.cursor) {
1414
- filters.push("(created_at < ? or (created_at = ? and id < ?))");
1415
- values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
1416
- }
1417
- const rows = db.prepare(`
1418
- select *
1419
- from ready_posts
1420
- where ${filters.join(" and ")}
1421
- order by created_at desc, id desc
1422
- ${input?.limit || input?.cursor ? "limit ?" : ""}
1423
- `).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 501))] : values));
1424
- return rows.map(mapReadyPost);
1425
- },
1426
- archiveReadyPost(customerId, postId) {
1427
- const timestamp = nowIso();
1428
- db.prepare(`
1429
- update ready_posts
1430
- set status = 'archived', archived_at = coalesce(archived_at, ?), updated_at = ?
1431
- where id = ? and customer_id = ? and status != 'deleted'
1432
- `).run(timestamp, timestamp, postId, customerId);
1433
- return this.getReadyPost(postId);
1434
- },
1435
- deleteReadyPost(customerId, postId) {
1436
- const timestamp = nowIso();
1437
- db.prepare(`
1438
- update ready_posts
1439
- set status = 'deleted', deleted_at = coalesce(deleted_at, ?), updated_at = ?
1440
- where id = ? and customer_id = ?
1441
- `).run(timestamp, timestamp, postId, customerId);
1442
- return this.getReadyPost(postId);
1443
- },
1444
- setReadyPostZip(input) {
1445
- db.prepare(`
1446
- update ready_posts
1447
- set zip_storage_key = ?, zip_public_url = ?, updated_at = ?
1448
- where id = ? and customer_id = ? and status != 'deleted'
1449
- `).run(input.storageKey, input.publicUrl, nowIso(), input.postId, input.customerId);
1450
- return this.getReadyPost(input.postId);
1451
- },
1452
- createPostSchedule(record) {
1453
- const timestamp = nowIso();
1454
- db.prepare(`
1455
- insert into post_schedules (
1456
- id, customer_id, ready_post_id, managed_by, provider, destination_type, destination_id,
1457
- scheduled_at, timezone, status, provider_status, provider_schedule_id, title, summary, url,
1458
- additional_notes, provider_payload_json, provider_response_json, last_synced_at, last_error, cancelled_at, sent_at,
1459
- created_at, updated_at
1460
- ) values (
1461
- @id, @customer_id, @ready_post_id, @managed_by, @provider, @destination_type, @destination_id,
1462
- @scheduled_at, @timezone, @status, @provider_status, @provider_schedule_id, @title, @summary, @url,
1463
- @additional_notes, @provider_payload_json, @provider_response_json, @last_synced_at, @last_error, @cancelled_at, @sent_at,
1464
- @created_at, @updated_at
1465
- )
1466
- `).run({
1467
- id: record.id,
1468
- customer_id: record.customerId,
1469
- ready_post_id: record.readyPostId,
1470
- managed_by: record.managedBy,
1471
- provider: record.provider,
1472
- destination_type: record.destinationType,
1473
- destination_id: record.destinationId,
1474
- scheduled_at: record.scheduledAt,
1475
- timezone: record.timezone,
1476
- status: record.status,
1477
- provider_status: record.providerStatus,
1478
- provider_schedule_id: record.providerScheduleId,
1479
- title: record.title,
1480
- summary: record.summary,
1481
- url: record.url,
1482
- additional_notes: record.additionalNotes,
1483
- provider_payload_json: stringifyJson(record.providerPayload),
1484
- provider_response_json: stringifyJson(record.providerResponse),
1485
- last_synced_at: record.lastSyncedAt,
1486
- last_error: record.lastError,
1487
- cancelled_at: record.cancelledAt,
1488
- sent_at: record.sentAt,
1489
- created_at: timestamp,
1490
- updated_at: timestamp
1491
- });
1492
- if (record.readyPostId) {
1493
- this.refreshReadyPostScheduleStatus(record.customerId, record.readyPostId);
1494
- }
1495
- return this.getPostSchedule(record.customerId, record.id);
1496
- },
1497
- getPostSchedule(customerId, scheduleId) {
1498
- const row = db.prepare(`
1499
- select *
1500
- from post_schedules
1501
- where customer_id = ? and id = ?
1502
- limit 1
1503
- `).get(customerId, scheduleId);
1504
- return row ? mapPostSchedule(row) : null;
1505
- },
1506
- listPostSchedulesForPost(customerId, postId) {
1507
- const rows = db.prepare(`
1508
- select *
1509
- from post_schedules
1510
- where customer_id = ? and ready_post_id = ?
1511
- order by scheduled_at asc, created_at asc
1512
- `).all(customerId, postId);
1513
- return rows.map(mapPostSchedule);
1514
- },
1515
- listPostSchedulesForCalendar(input) {
1516
- const rows = db.prepare(`
1517
- select *
1518
- from post_schedules
1519
- where customer_id = ?
1520
- and scheduled_at >= ?
1521
- and scheduled_at <= ?
1522
- and status != 'cancelled'
1523
- order by scheduled_at asc, created_at asc
1524
- `).all(input.customerId, input.startDate, input.endDate);
1525
- return rows.map(mapPostSchedule);
1526
- },
1527
- updatePostScheduleProvider(input) {
1528
- db.prepare(`
1529
- update post_schedules
1530
- set scheduled_at = ?,
1531
- timezone = ?,
1532
- status = ?,
1533
- provider_status = ?,
1534
- provider_schedule_id = ?,
1535
- provider_payload_json = ?,
1536
- provider_response_json = ?,
1537
- last_synced_at = ?,
1538
- last_error = ?,
1539
- url = coalesce(?, url),
1540
- additional_notes = ?,
1541
- cancelled_at = null,
1542
- updated_at = ?
1543
- where customer_id = ? and id = ?
1544
- `).run(input.scheduledAt, input.timezone ?? null, input.status ?? "scheduled", input.providerStatus ?? null, input.providerScheduleId ?? null, stringifyJson(input.providerPayload), stringifyJson(input.providerResponse), nowIso(), input.lastError ?? null, input.url ?? null, input.additionalNotes ?? null, nowIso(), input.customerId, input.scheduleId);
1545
- const updated = this.getPostSchedule(input.customerId, input.scheduleId);
1546
- if (updated?.readyPostId) {
1547
- this.refreshReadyPostScheduleStatus(input.customerId, updated.readyPostId);
1548
- }
1549
- return updated;
1550
- },
1551
- markPostScheduleCancelled(input) {
1552
- const timestamp = nowIso();
1553
- const existing = this.getPostSchedule(input.customerId, input.scheduleId);
1554
- db.prepare(`
1555
- update post_schedules
1556
- set status = 'cancelled',
1557
- provider_response_json = ?,
1558
- last_synced_at = ?,
1559
- last_error = ?,
1560
- cancelled_at = coalesce(cancelled_at, ?),
1561
- updated_at = ?
1562
- where customer_id = ? and id = ?
1563
- `).run(stringifyJson(input.providerResponse ?? existing?.providerResponse ?? {}), timestamp, input.lastError ?? null, timestamp, timestamp, input.customerId, input.scheduleId);
1564
- if (existing?.readyPostId) {
1565
- this.refreshReadyPostScheduleStatus(input.customerId, existing.readyPostId);
1566
- }
1567
- return this.getPostSchedule(input.customerId, input.scheduleId);
1568
- },
1569
- markPostScheduleFailed(input) {
1570
- const existing = this.getPostSchedule(input.customerId, input.scheduleId);
1571
- db.prepare(`
1572
- update post_schedules
1573
- set status = 'failed',
1574
- provider_response_json = ?,
1575
- last_synced_at = ?,
1576
- last_error = ?,
1577
- updated_at = ?
1578
- where customer_id = ? and id = ?
1579
- `).run(stringifyJson(input.providerResponse ?? existing?.providerResponse ?? {}), nowIso(), input.lastError, nowIso(), input.customerId, input.scheduleId);
1580
- if (existing?.readyPostId) {
1581
- this.refreshReadyPostScheduleStatus(input.customerId, existing.readyPostId);
1582
- }
1583
- return this.getPostSchedule(input.customerId, input.scheduleId);
1584
- },
1585
- upsertExternalPostSchedule(record) {
1586
- const timestamp = nowIso();
1587
- const existing = record.providerScheduleId
1588
- ? db.prepare(`
1589
- select *
1590
- from post_schedules
1591
- where customer_id = ? and provider = ? and provider_schedule_id = ?
1592
- limit 1
1593
- `).get(record.customerId, record.provider, record.providerScheduleId)
1594
- : undefined;
1595
- if (existing) {
1596
- db.prepare(`
1597
- update post_schedules
1598
- set destination_id = ?,
1599
- scheduled_at = ?,
1600
- status = ?,
1601
- provider_status = ?,
1602
- title = ?,
1603
- summary = ?,
1604
- url = ?,
1605
- additional_notes = ?,
1606
- provider_payload_json = ?,
1607
- provider_response_json = ?,
1608
- last_synced_at = ?,
1609
- last_error = null,
1610
- updated_at = ?
1611
- where id = ?
1612
- `).run(record.destinationId, record.scheduledAt, record.status, record.providerStatus, record.title, record.summary, record.url, record.additionalNotes ?? null, stringifyJson(record.providerPayload), stringifyJson(record.providerResponse), timestamp, timestamp, String(existing.id));
1613
- return this.getPostSchedule(record.customerId, String(existing.id));
1614
- }
1615
- return this.createPostSchedule({
1616
- ...record,
1617
- readyPostId: null,
1618
- managedBy: "external",
1619
- timezone: null,
1620
- additionalNotes: record.additionalNotes ?? null,
1621
- cancelledAt: null,
1622
- sentAt: null,
1623
- lastError: null
1624
- });
1625
- },
1626
- refreshReadyPostScheduleStatus(customerId, postId) {
1627
- const row = db.prepare(`
1628
- select count(*) as count
1629
- from post_schedules
1630
- where customer_id = ? and ready_post_id = ? and status = 'scheduled'
1631
- `).get(customerId, postId);
1632
- const nextStatus = row && Number(row.count) > 0 ? "scheduled" : "ready";
1633
- db.prepare(`
1634
- update ready_posts
1635
- set status = ?, updated_at = ?
1636
- where customer_id = ? and id = ? and status in ('ready', 'scheduled')
1637
- `).run(nextStatus, nowIso(), customerId, postId);
1638
- return this.getReadyPost(postId);
1639
- },
1640
- createTemplateSource(record) {
1641
- const timestamp = nowIso();
1642
- db.prepare(`
1643
- insert into template_sources (
1644
- id, template_id, slug_id, repo_url, branch, template_module_path, skill_path, install_command, build_command, status, created_at, updated_at
1645
- ) values (
1646
- @id, @template_id, @slug_id, @repo_url, @branch, @template_module_path, @skill_path, @install_command, @build_command, @status, @created_at, @updated_at
1647
- )
1648
- `).run({
1649
- id: record.id,
1650
- template_id: record.templateId,
1651
- slug_id: record.slugId,
1652
- repo_url: record.repoUrl,
1653
- branch: record.branch,
1654
- template_module_path: record.templateModulePath,
1655
- skill_path: record.skillPath,
1656
- install_command: record.installCommand,
1657
- build_command: record.buildCommand,
1658
- status: record.status,
1659
- created_at: timestamp,
1660
- updated_at: timestamp
1661
- });
1662
- return this.getTemplateSourceByTemplateId(record.templateId);
1663
- },
1664
- updateTemplateSource(input) {
1665
- const current = this.getTemplateSource(input.id);
1666
- if (!current) {
1667
- throw new Error("Template source not found.");
1668
- }
1669
- db.prepare(`
1670
- update template_sources
1671
- set repo_url = @repo_url,
1672
- branch = @branch,
1673
- template_module_path = @template_module_path,
1674
- skill_path = @skill_path,
1675
- install_command = @install_command,
1676
- build_command = @build_command,
1677
- status = @status,
1678
- updated_at = @updated_at
1679
- where id = @id
1680
- `).run({
1681
- id: input.id,
1682
- repo_url: input.repoUrl,
1683
- branch: input.branch,
1684
- template_module_path: input.templateModulePath,
1685
- skill_path: input.skillPath,
1686
- install_command: input.installCommand,
1687
- build_command: input.buildCommand,
1688
- status: input.status ?? current.status,
1689
- updated_at: nowIso()
1690
- });
1691
- return this.getTemplateSource(input.id);
1692
- },
1693
- getTemplateSource(id) {
1694
- const row = db.prepare(`select * from template_sources where id = ?`).get(id);
1695
- return row ? mapTemplateSource(row) : null;
1696
- },
1697
- getTemplateSourceByTemplateId(templateId) {
1698
- const row = db.prepare(`select * from template_sources where template_id = ?`).get(templateId);
1699
- return row ? mapTemplateSource(row) : null;
1700
- },
1701
- getTemplateSourceBySlugId(slugId) {
1702
- const row = db.prepare(`select * from template_sources where slug_id = ?`).get(slugId);
1703
- return row ? mapTemplateSource(row) : null;
1704
- },
1705
- getTemplateSourceByLocation(repoUrl, branch, templateModulePath) {
1706
- const row = db.prepare(`
1707
- select *
1708
- from template_sources
1709
- where repo_url = ? and branch = ? and template_module_path = ?
1710
- limit 1
1711
- `).get(repoUrl, branch, templateModulePath);
1712
- return row ? mapTemplateSource(row) : null;
1713
- },
1714
- listTemplateSources(input) {
1715
- const filters = [];
1716
- const values = [];
1717
- if (input?.status) {
1718
- filters.push("status = ?");
1719
- values.push(input.status);
1720
- }
1721
- if (input?.cursor) {
1722
- filters.push("(created_at < ? or (created_at = ? and id < ?))");
1723
- values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
1724
- }
1725
- const rows = db.prepare(`
1726
- select *
1727
- from template_sources
1728
- ${filters.length ? `where ${filters.join(" and ")}` : ""}
1729
- order by created_at desc, id desc
1730
- ${input?.limit || input?.cursor ? "limit ?" : ""}
1731
- `).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 501))] : values));
1732
- return rows.map(mapTemplateSource);
1733
- },
1734
- createTemplateRelease(record) {
1735
- const timestamp = nowIso();
1736
- db.prepare(`
1737
- insert into template_releases (
1738
- id, source_id, template_id, branch, commit_sha, checkout_path, skill_path, module_path, artifact_manifest_key, artifact_key, status,
1739
- certification_report_json, activated_at, created_at, updated_at
1740
- ) values (
1741
- @id, @source_id, @template_id, @branch, @commit_sha, @checkout_path, @skill_path, @module_path, @artifact_manifest_key, @artifact_key, @status,
1742
- @certification_report_json, @activated_at, @created_at, @updated_at
1743
- )
1744
- on conflict(source_id, commit_sha) do update set
1745
- checkout_path = excluded.checkout_path,
1746
- skill_path = excluded.skill_path,
1747
- module_path = excluded.module_path,
1748
- artifact_manifest_key = excluded.artifact_manifest_key,
1749
- artifact_key = excluded.artifact_key,
1750
- status = excluded.status,
1751
- certification_report_json = excluded.certification_report_json,
1752
- activated_at = excluded.activated_at,
1753
- updated_at = excluded.updated_at
1754
- `).run({
1755
- id: record.id,
1756
- source_id: record.sourceId,
1757
- template_id: record.templateId,
1758
- branch: record.branch,
1759
- commit_sha: record.commitSha,
1760
- checkout_path: record.checkoutPath,
1761
- skill_path: record.skillPath,
1762
- module_path: record.modulePath,
1763
- artifact_manifest_key: record.artifactManifestKey ?? null,
1764
- artifact_key: record.artifactKey ?? null,
1765
- status: record.status,
1766
- certification_report_json: stringifyJson(record.certificationReport ?? null),
1767
- activated_at: record.activatedAt ?? null,
1768
- created_at: timestamp,
1769
- updated_at: timestamp
1770
- });
1771
- return this.getTemplateReleaseBySourceAndCommit(record.sourceId, record.commitSha);
1772
- },
1773
- getTemplateRelease(id) {
1774
- const row = db.prepare(`select * from template_releases where id = ?`).get(id);
1775
- return row ? mapTemplateRelease(row) : null;
1776
- },
1777
- getTemplateReleaseBySourceAndCommit(sourceId, commitSha) {
1778
- const row = db.prepare(`select * from template_releases where source_id = ? and commit_sha = ?`).get(sourceId, commitSha);
1779
- return row ? mapTemplateRelease(row) : null;
1780
- },
1781
- getLatestTemplateReleaseForSource(sourceId) {
1782
- const row = db.prepare(`
1783
- select *
1784
- from template_releases
1785
- where source_id = ?
1786
- order by created_at desc
1787
- limit 1
1788
- `).get(sourceId);
1789
- return row ? mapTemplateRelease(row) : null;
1790
- },
1791
- listTemplateReleases(input) {
1792
- const filters = [];
1793
- const values = [];
1794
- if (input?.templateId) {
1795
- filters.push("template_id = ?");
1796
- values.push(input.templateId);
1797
- }
1798
- if (input?.status) {
1799
- filters.push("status = ?");
1800
- values.push(input.status);
1801
- }
1802
- if (input?.cursor) {
1803
- filters.push("(created_at < ? or (created_at = ? and id < ?))");
1804
- values.push(input.cursor.createdAt, input.cursor.createdAt, input.cursor.id);
1805
- }
1806
- const rows = db.prepare(`
1807
- select *
1808
- from template_releases
1809
- ${filters.length ? `where ${filters.join(" and ")}` : ""}
1810
- order by created_at desc, id desc
1811
- ${input?.limit || input?.cursor ? "limit ?" : ""}
1812
- `).all(...(input?.limit || input?.cursor ? [...values, Math.max(1, Math.min(input?.limit ?? 100, 500))] : values));
1813
- return rows.map(mapTemplateRelease);
1814
- },
1815
- getActiveTemplateReleases() {
1816
- return db.prepare(`select * from template_releases where status = 'active' order by template_id asc`).all().map(mapTemplateRelease);
1817
- },
1818
- updateTemplateReleaseStatus(input) {
1819
- const current = this.getTemplateRelease(input.id);
1820
- db.prepare(`
1821
- update template_releases
1822
- set status = @status,
1823
- certification_report_json = @certification_report_json,
1824
- activated_at = @activated_at,
1825
- updated_at = @updated_at
1826
- where id = @id
1827
- `).run({
1828
- id: input.id,
1829
- status: input.status,
1830
- certification_report_json: input.certificationReport === undefined
1831
- ? stringifyJson(current?.certificationReport ?? null)
1832
- : stringifyJson(input.certificationReport),
1833
- activated_at: input.activatedAt === undefined ? current?.activatedAt ?? null : input.activatedAt,
1834
- updated_at: nowIso()
1835
- });
1836
- },
1837
- clearActiveTemplateReleases(templateId, fallbackStatus = "approved") {
1838
- db.prepare(`
1839
- update template_releases
1840
- set status = case when status = 'active' then @fallback_status else status end,
1841
- updated_at = @updated_at
1842
- where template_id = @template_id
1843
- `).run({
1844
- fallback_status: fallbackStatus,
1845
- updated_at: nowIso(),
1846
- template_id: templateId
1847
- });
1848
- },
1849
- upsertChatThread(input) {
1850
- const timestamp = nowIso();
1851
- const current = this.getChatThreadMeta(input.customerId, input.id);
1852
- db.prepare(`
1853
- insert into chat_threads (
1854
- id, customer_id, template_id, title, storage_prefix, message_count, last_message_at,
1855
- hidden_from_view, archived_at, created_at, updated_at
1856
- ) values (
1857
- @id, @customer_id, @template_id, @title, @storage_prefix, @message_count, @last_message_at,
1858
- @hidden_from_view, @archived_at, @created_at, @updated_at
1859
- )
1860
- on conflict(id) do update set
1861
- template_id = excluded.template_id,
1862
- title = excluded.title,
1863
- storage_prefix = excluded.storage_prefix,
1864
- message_count = excluded.message_count,
1865
- last_message_at = excluded.last_message_at,
1866
- hidden_from_view = excluded.hidden_from_view,
1867
- archived_at = excluded.archived_at,
1868
- updated_at = excluded.updated_at
1869
- `).run({
1870
- id: input.id,
1871
- customer_id: input.customerId,
1872
- template_id: input.templateId,
1873
- title: input.title,
1874
- storage_prefix: input.storagePrefix,
1875
- message_count: input.messageCount ?? current?.messageCount ?? 0,
1876
- last_message_at: input.lastMessageAt ?? current?.lastMessageAt ?? null,
1877
- hidden_from_view: input.hiddenFromView === undefined ? (current?.hiddenFromView ? 1 : 0) : (input.hiddenFromView ? 1 : 0),
1878
- archived_at: input.archivedAt === undefined ? (current?.archivedAt ?? null) : input.archivedAt,
1879
- created_at: current?.createdAt ?? timestamp,
1880
- updated_at: timestamp
1881
- });
1882
- if (input.tracers) {
1883
- this.replaceChatThreadTracers(input.id, input.tracers);
1884
- }
1885
- return this.getChatThreadMeta(input.customerId, input.id);
1886
- },
1887
- appendChatThreadMessages(input) {
1888
- const current = this.getChatThreadMeta(input.customerId, input.threadId);
1889
- this.upsertChatThread({
1890
- id: input.threadId,
1891
- customerId: input.customerId,
1892
- templateId: input.templateId,
1893
- title: input.title || current?.title || "New chat",
1894
- storagePrefix: input.storagePrefix,
1895
- tracers: input.tracers,
1896
- hiddenFromView: current?.hiddenFromView ?? false,
1897
- archivedAt: current?.archivedAt ?? null,
1898
- messageCount: (current?.messageCount ?? 0) + Math.max(0, input.messageCountDelta),
1899
- lastMessageAt: input.lastMessageAt ?? current?.lastMessageAt ?? null
1900
- });
1901
- return this.getChatThreadMeta(input.customerId, input.threadId);
1902
- },
1903
- getChatThreadMeta(customerId, threadId) {
1904
- const row = db.prepare(`
1905
- select *
1906
- from chat_threads
1907
- where customer_id = ? and id = ?
1908
- limit 1
1909
- `).get(customerId, threadId);
1910
- if (!row) {
1911
- return null;
1912
- }
1913
- const tracers = db.prepare(`
1914
- select tracer
1915
- from chat_thread_tracers
1916
- where thread_id = ?
1917
- order by created_at asc, tracer asc
1918
- `).all(threadId);
1919
- return mapChatThread(row, tracers.map((entry) => String(entry.tracer)));
1920
- },
1921
- listChatThreadMetas(input) {
1922
- const filters = ["customer_id = ?"];
1923
- const values = [input.customerId];
1924
- if (input.templateId) {
1925
- filters.push("template_id = ?");
1926
- values.push(input.templateId);
1927
- }
1928
- if (input.cursor) {
1929
- filters.push("(updated_at < ? or (updated_at = ? and id < ?))");
1930
- values.push(input.cursor.updatedAt, input.cursor.updatedAt, input.cursor.id);
1931
- }
1932
- values.push(Math.max(1, Math.min(input.limit ?? 100, 501)));
1933
- const rows = db.prepare(`
1934
- select *
1935
- from chat_threads
1936
- where ${filters.join(" and ")}
1937
- order by updated_at desc, id desc
1938
- limit ?
1939
- `).all(...values);
1940
- if (!rows.length) {
1941
- return [];
1942
- }
1943
- const tracerRows = db.prepare(`
1944
- select thread_id, tracer
1945
- from chat_thread_tracers
1946
- where thread_id in (${rows.map(() => "?").join(", ")})
1947
- order by created_at asc, tracer asc
1948
- `).all(...rows.map((row) => String(row.id)));
1949
- const tracerMap = new Map();
1950
- for (const row of tracerRows) {
1951
- const list = tracerMap.get(String(row.thread_id)) ?? [];
1952
- list.push(String(row.tracer));
1953
- tracerMap.set(String(row.thread_id), list);
1954
- }
1955
- return rows.map((row) => mapChatThread(row, tracerMap.get(String(row.id)) ?? []));
1956
- },
1957
- replaceChatThreadTracers(threadId, tracers) {
1958
- const normalized = [...new Set(tracers.map((value) => value.trim()).filter(Boolean))];
1959
- const timestamp = nowIso();
1960
- const transaction = db.transaction(() => {
1961
- db.prepare(`delete from chat_thread_tracers where thread_id = ?`).run(threadId);
1962
- const insert = db.prepare(`
1963
- insert into chat_thread_tracers (thread_id, tracer, created_at)
1964
- values (?, ?, ?)
1965
- `);
1966
- for (const tracer of normalized) {
1967
- insert.run(threadId, tracer, timestamp);
1968
- }
1969
- });
1970
- transaction();
1971
- },
1972
- updateChatThreadState(input) {
1973
- const current = this.getChatThreadMeta(input.customerId, input.threadId);
1974
- if (!current) {
1975
- return null;
1976
- }
1977
- db.prepare(`
1978
- update chat_threads
1979
- set title = ?,
1980
- hidden_from_view = ?,
1981
- archived_at = ?,
1982
- updated_at = ?
1983
- where customer_id = ? and id = ?
1984
- `).run(input.title ?? current.title, input.hiddenFromView === undefined ? (current.hiddenFromView ? 1 : 0) : (input.hiddenFromView ? 1 : 0), input.archivedAt === undefined ? current.archivedAt : input.archivedAt, nowIso(), input.customerId, input.threadId);
1985
- if (input.tracers) {
1986
- this.replaceChatThreadTracers(input.threadId, input.tracers);
1987
- }
1988
- return this.getChatThreadMeta(input.customerId, input.threadId);
1989
- },
1990
- deleteChatThread(customerId, threadId) {
1991
- const current = this.getChatThreadMeta(customerId, threadId);
1992
- if (!current) {
1993
- return null;
1994
- }
1995
- db.prepare(`
1996
- delete from chat_threads
1997
- where customer_id = ? and id = ?
1998
- `).run(customerId, threadId);
1999
- return current;
2000
- }
2001
- };
2002
- function normalizeCustomerPlanTier(value) {
2003
- if (value === "starter" || value === "standard" || value === "agency") {
2004
- return value;
2005
- }
2006
- return "starter";
2007
- }