@intelligo-dev/core 1.0.0-beta.1 → 1.0.0-beta.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 (355) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +102 -0
  3. package/dist/attachments/errors.d.ts +7 -0
  4. package/dist/attachments/errors.d.ts.map +1 -0
  5. package/dist/attachments/errors.js +11 -0
  6. package/dist/attachments/errors.js.map +1 -0
  7. package/dist/attachments/index.d.ts +8 -0
  8. package/dist/attachments/index.d.ts.map +1 -0
  9. package/dist/attachments/index.js +7 -0
  10. package/dist/attachments/index.js.map +1 -0
  11. package/dist/attachments/service.d.ts +45 -0
  12. package/dist/attachments/service.d.ts.map +1 -0
  13. package/dist/attachments/service.js +100 -0
  14. package/dist/attachments/service.js.map +1 -0
  15. package/dist/attachments/types.d.ts +6 -0
  16. package/dist/attachments/types.d.ts.map +1 -0
  17. package/dist/attachments/types.js +2 -0
  18. package/dist/attachments/types.js.map +1 -0
  19. package/dist/conversations/errors.d.ts +2 -8
  20. package/dist/conversations/errors.d.ts.map +1 -1
  21. package/dist/conversations/errors.js +2 -8
  22. package/dist/conversations/errors.js.map +1 -1
  23. package/dist/conversations/index.d.ts +4 -12
  24. package/dist/conversations/index.d.ts.map +1 -1
  25. package/dist/conversations/index.js +2 -11
  26. package/dist/conversations/index.js.map +1 -1
  27. package/dist/conversations/service.d.ts +49 -61
  28. package/dist/conversations/service.d.ts.map +1 -1
  29. package/dist/conversations/service.js +110 -83
  30. package/dist/conversations/service.js.map +1 -1
  31. package/dist/conversations/types.d.ts +1 -4
  32. package/dist/conversations/types.d.ts.map +1 -1
  33. package/dist/conversations/types.js +0 -3
  34. package/dist/conversations/types.js.map +1 -1
  35. package/dist/db/client.d.ts +21 -10
  36. package/dist/db/client.d.ts.map +1 -1
  37. package/dist/db/client.js +43 -18
  38. package/dist/db/client.js.map +1 -1
  39. package/dist/db/index.d.ts +3 -3
  40. package/dist/db/index.d.ts.map +1 -1
  41. package/dist/db/index.js +3 -7
  42. package/dist/db/index.js.map +1 -1
  43. package/dist/db/schema/agents.d.ts +14 -51
  44. package/dist/db/schema/agents.d.ts.map +1 -1
  45. package/dist/db/schema/agents.js +9 -21
  46. package/dist/db/schema/agents.js.map +1 -1
  47. package/dist/db/schema/ai.d.ts +86 -1556
  48. package/dist/db/schema/ai.d.ts.map +1 -1
  49. package/dist/db/schema/ai.js +32 -236
  50. package/dist/db/schema/ai.js.map +1 -1
  51. package/dist/db/schema/auth.d.ts +5 -34
  52. package/dist/db/schema/auth.d.ts.map +1 -1
  53. package/dist/db/schema/auth.js +25 -56
  54. package/dist/db/schema/auth.js.map +1 -1
  55. package/dist/db/schema/billing.d.ts +136 -107
  56. package/dist/db/schema/billing.d.ts.map +1 -1
  57. package/dist/db/schema/billing.js +72 -60
  58. package/dist/db/schema/billing.js.map +1 -1
  59. package/dist/db/schema/feature-flags.d.ts +3 -12
  60. package/dist/db/schema/feature-flags.d.ts.map +1 -1
  61. package/dist/db/schema/feature-flags.js +3 -12
  62. package/dist/db/schema/feature-flags.js.map +1 -1
  63. package/dist/db/schema/identity.d.ts +12 -274
  64. package/dist/db/schema/identity.d.ts.map +1 -1
  65. package/dist/db/schema/identity.js +19 -104
  66. package/dist/db/schema/identity.js.map +1 -1
  67. package/dist/db/schema/index.d.ts +8 -15
  68. package/dist/db/schema/index.d.ts.map +1 -1
  69. package/dist/db/schema/index.js +12 -42
  70. package/dist/db/schema/index.js.map +1 -1
  71. package/dist/db/schema/notifications.d.ts +3 -17
  72. package/dist/db/schema/notifications.d.ts.map +1 -1
  73. package/dist/db/schema/notifications.js +3 -17
  74. package/dist/db/schema/notifications.js.map +1 -1
  75. package/dist/db/schema/usage.d.ts +94 -77
  76. package/dist/db/schema/usage.d.ts.map +1 -1
  77. package/dist/db/schema/usage.js +59 -89
  78. package/dist/db/schema/usage.js.map +1 -1
  79. package/dist/db/validate.d.ts +2 -11
  80. package/dist/db/validate.d.ts.map +1 -1
  81. package/dist/db/validate.js +6 -17
  82. package/dist/db/validate.js.map +1 -1
  83. package/dist/db/workspace-queries.d.ts +5 -29
  84. package/dist/db/workspace-queries.d.ts.map +1 -1
  85. package/dist/db/workspace-queries.js +5 -29
  86. package/dist/db/workspace-queries.js.map +1 -1
  87. package/dist/documents/classifier.d.ts +5 -12
  88. package/dist/documents/classifier.d.ts.map +1 -1
  89. package/dist/documents/classifier.js +16 -16
  90. package/dist/documents/classifier.js.map +1 -1
  91. package/dist/documents/errors.d.ts +2 -10
  92. package/dist/documents/errors.d.ts.map +1 -1
  93. package/dist/documents/errors.js +2 -10
  94. package/dist/documents/errors.js.map +1 -1
  95. package/dist/documents/index.d.ts +4 -13
  96. package/dist/documents/index.d.ts.map +1 -1
  97. package/dist/documents/index.js +3 -12
  98. package/dist/documents/index.js.map +1 -1
  99. package/dist/documents/service.d.ts +19 -26
  100. package/dist/documents/service.d.ts.map +1 -1
  101. package/dist/documents/service.js +44 -38
  102. package/dist/documents/service.js.map +1 -1
  103. package/dist/documents/types.d.ts +6 -10
  104. package/dist/documents/types.d.ts.map +1 -1
  105. package/dist/documents/types.js +0 -3
  106. package/dist/documents/types.js.map +1 -1
  107. package/dist/email/brand.d.ts +17 -0
  108. package/dist/email/brand.d.ts.map +1 -0
  109. package/dist/email/brand.js +9 -0
  110. package/dist/email/brand.js.map +1 -0
  111. package/dist/email/index.d.ts +4 -4
  112. package/dist/email/index.js +4 -4
  113. package/dist/email/provider.d.ts +1 -7
  114. package/dist/email/provider.d.ts.map +1 -1
  115. package/dist/email/provider.js +10 -19
  116. package/dist/email/provider.js.map +1 -1
  117. package/dist/email/send.d.ts +12 -12
  118. package/dist/email/send.d.ts.map +1 -1
  119. package/dist/email/send.js +5 -19
  120. package/dist/email/send.js.map +1 -1
  121. package/dist/email/senders.d.ts +17 -30
  122. package/dist/email/senders.d.ts.map +1 -1
  123. package/dist/email/senders.js +33 -47
  124. package/dist/email/senders.js.map +1 -1
  125. package/dist/email/templates/base-layout.d.ts +1 -1
  126. package/dist/email/templates/base-layout.d.ts.map +1 -1
  127. package/dist/email/templates/base-layout.js +4 -6
  128. package/dist/email/templates/base-layout.js.map +1 -1
  129. package/dist/email/templates/index.d.ts +10 -10
  130. package/dist/email/templates/index.d.ts.map +1 -1
  131. package/dist/email/templates/index.js +10 -16
  132. package/dist/email/templates/index.js.map +1 -1
  133. package/dist/email/templates/password-reset.d.ts +2 -2
  134. package/dist/email/templates/password-reset.d.ts.map +1 -1
  135. package/dist/email/templates/password-reset.js +10 -22
  136. package/dist/email/templates/password-reset.js.map +1 -1
  137. package/dist/email/templates/payment-failed.d.ts +2 -2
  138. package/dist/email/templates/payment-failed.d.ts.map +1 -1
  139. package/dist/email/templates/payment-failed.js +11 -24
  140. package/dist/email/templates/payment-failed.js.map +1 -1
  141. package/dist/email/templates/quota-warning.d.ts +4 -4
  142. package/dist/email/templates/quota-warning.d.ts.map +1 -1
  143. package/dist/email/templates/quota-warning.js +19 -39
  144. package/dist/email/templates/quota-warning.js.map +1 -1
  145. package/dist/email/templates/subscription-confirmed.d.ts +2 -2
  146. package/dist/email/templates/subscription-confirmed.d.ts.map +1 -1
  147. package/dist/email/templates/subscription-confirmed.js +13 -28
  148. package/dist/email/templates/subscription-confirmed.js.map +1 -1
  149. package/dist/email/templates/trial-expiry.d.ts +2 -2
  150. package/dist/email/templates/trial-expiry.d.ts.map +1 -1
  151. package/dist/email/templates/trial-expiry.js +12 -36
  152. package/dist/email/templates/trial-expiry.js.map +1 -1
  153. package/dist/email/templates/trial-warning.d.ts +2 -2
  154. package/dist/email/templates/trial-warning.d.ts.map +1 -1
  155. package/dist/email/templates/trial-warning.js +15 -32
  156. package/dist/email/templates/trial-warning.js.map +1 -1
  157. package/dist/email/templates/verify-email.d.ts +2 -2
  158. package/dist/email/templates/verify-email.d.ts.map +1 -1
  159. package/dist/email/templates/verify-email.js +10 -22
  160. package/dist/email/templates/verify-email.js.map +1 -1
  161. package/dist/email/templates/welcome.d.ts +2 -2
  162. package/dist/email/templates/welcome.d.ts.map +1 -1
  163. package/dist/email/templates/welcome.js +13 -20
  164. package/dist/email/templates/welcome.js.map +1 -1
  165. package/dist/email/templates/workspace-invitation.d.ts +2 -2
  166. package/dist/email/templates/workspace-invitation.d.ts.map +1 -1
  167. package/dist/email/templates/workspace-invitation.js +12 -26
  168. package/dist/email/templates/workspace-invitation.js.map +1 -1
  169. package/dist/env.d.ts +5 -10
  170. package/dist/env.d.ts.map +1 -1
  171. package/dist/env.js +41 -31
  172. package/dist/env.js.map +1 -1
  173. package/dist/identity/audit.d.ts +11 -24
  174. package/dist/identity/audit.d.ts.map +1 -1
  175. package/dist/identity/audit.js +13 -27
  176. package/dist/identity/audit.js.map +1 -1
  177. package/dist/identity/errors.d.ts +4 -12
  178. package/dist/identity/errors.d.ts.map +1 -1
  179. package/dist/identity/errors.js +2 -8
  180. package/dist/identity/errors.js.map +1 -1
  181. package/dist/identity/index.d.ts +5 -13
  182. package/dist/identity/index.d.ts.map +1 -1
  183. package/dist/identity/index.js +4 -12
  184. package/dist/identity/index.js.map +1 -1
  185. package/dist/identity/service.d.ts +13 -47
  186. package/dist/identity/service.d.ts.map +1 -1
  187. package/dist/identity/service.js +31 -78
  188. package/dist/identity/service.js.map +1 -1
  189. package/dist/identity/types.d.ts +1 -4
  190. package/dist/identity/types.d.ts.map +1 -1
  191. package/dist/identity/types.js +0 -3
  192. package/dist/identity/types.js.map +1 -1
  193. package/dist/index.d.ts +1 -2
  194. package/dist/index.d.ts.map +1 -1
  195. package/dist/index.js +3 -10
  196. package/dist/index.js.map +1 -1
  197. package/dist/logger.d.ts +7 -18
  198. package/dist/logger.d.ts.map +1 -1
  199. package/dist/logger.js +45 -49
  200. package/dist/logger.js.map +1 -1
  201. package/dist/money.d.ts +95 -0
  202. package/dist/money.d.ts.map +1 -0
  203. package/dist/money.js +208 -0
  204. package/dist/money.js.map +1 -0
  205. package/dist/notifications/index.d.ts +9 -43
  206. package/dist/notifications/index.d.ts.map +1 -1
  207. package/dist/notifications/index.js +9 -49
  208. package/dist/notifications/index.js.map +1 -1
  209. package/dist/notifications/triggers.d.ts +9 -32
  210. package/dist/notifications/triggers.d.ts.map +1 -1
  211. package/dist/notifications/triggers.js +21 -58
  212. package/dist/notifications/triggers.js.map +1 -1
  213. package/dist/notifications/types.d.ts +1 -10
  214. package/dist/notifications/types.d.ts.map +1 -1
  215. package/dist/notifications/types.js +0 -9
  216. package/dist/notifications/types.js.map +1 -1
  217. package/dist/prompt.d.ts +38 -0
  218. package/dist/prompt.d.ts.map +1 -0
  219. package/dist/prompt.js +118 -0
  220. package/dist/prompt.js.map +1 -0
  221. package/dist/registry.d.ts +32 -0
  222. package/dist/registry.d.ts.map +1 -0
  223. package/dist/registry.js +60 -0
  224. package/dist/registry.js.map +1 -0
  225. package/dist/request-context.d.ts +49 -0
  226. package/dist/request-context.d.ts.map +1 -0
  227. package/dist/request-context.js +100 -0
  228. package/dist/request-context.js.map +1 -0
  229. package/dist/storage/index.d.ts +65 -0
  230. package/dist/storage/index.d.ts.map +1 -0
  231. package/dist/storage/index.js +109 -0
  232. package/dist/storage/index.js.map +1 -0
  233. package/package.json +74 -24
  234. package/src/attachments/errors.ts +18 -0
  235. package/src/attachments/index.ts +22 -0
  236. package/src/attachments/service.ts +178 -0
  237. package/src/attachments/types.ts +6 -0
  238. package/src/conversations/errors.ts +45 -0
  239. package/src/conversations/index.ts +39 -0
  240. package/src/conversations/service.ts +594 -0
  241. package/src/conversations/types.ts +15 -0
  242. package/src/db/client.ts +91 -0
  243. package/src/db/index.ts +3 -0
  244. package/src/db/migrations/0000_baseline.sql +652 -0
  245. package/src/db/migrations/0001_reconcile_chain_built_databases.sql +53 -0
  246. package/src/db/migrations/0002_webhook_claim_lease.sql +1 -0
  247. package/src/db/migrations/README.md +74 -36
  248. package/src/db/migrations/legacy-chain.json +197 -0
  249. package/src/db/migrations/meta/0000_snapshot.json +4182 -157
  250. package/src/db/migrations/meta/0002_snapshot.json +2478 -368
  251. package/src/db/migrations/meta/_journal.json +6 -279
  252. package/src/db/schema/agents.ts +57 -0
  253. package/src/db/schema/ai.ts +174 -0
  254. package/src/db/schema/auth.ts +183 -0
  255. package/src/db/schema/billing.ts +209 -0
  256. package/src/db/schema/feature-flags.ts +20 -0
  257. package/src/db/schema/identity.ts +250 -0
  258. package/src/db/schema/index.ts +12 -0
  259. package/src/db/schema/notifications.ts +31 -0
  260. package/src/db/schema/usage.ts +269 -0
  261. package/src/db/validate.ts +63 -0
  262. package/src/db/workspace-queries.ts +64 -0
  263. package/src/documents/classifier.ts +61 -0
  264. package/src/documents/errors.ts +44 -0
  265. package/src/documents/index.ts +24 -0
  266. package/src/documents/service.ts +256 -0
  267. package/src/documents/types.ts +24 -0
  268. package/src/email/brand.ts +24 -0
  269. package/src/email/index.ts +19 -0
  270. package/src/email/provider.ts +278 -0
  271. package/src/email/send.ts +169 -0
  272. package/src/email/senders.ts +292 -0
  273. package/src/email/templates/base-layout.tsx +145 -0
  274. package/src/email/templates/index.ts +28 -0
  275. package/src/email/templates/password-reset.tsx +72 -0
  276. package/src/email/templates/payment-failed.tsx +77 -0
  277. package/src/email/templates/quota-warning.tsx +118 -0
  278. package/src/email/templates/subscription-confirmed.tsx +91 -0
  279. package/src/email/templates/trial-expiry.tsx +124 -0
  280. package/src/email/templates/trial-warning.tsx +104 -0
  281. package/src/email/templates/verify-email.tsx +72 -0
  282. package/src/email/templates/welcome.tsx +64 -0
  283. package/src/email/templates/workspace-invitation.tsx +105 -0
  284. package/src/env.ts +180 -0
  285. package/src/identity/audit.ts +65 -0
  286. package/src/identity/errors.ts +44 -0
  287. package/src/identity/index.ts +26 -0
  288. package/src/identity/service.ts +202 -0
  289. package/src/identity/types.ts +28 -0
  290. package/src/index.ts +4 -0
  291. package/src/logger.ts +198 -0
  292. package/src/money.ts +270 -0
  293. package/src/notifications/index.ts +108 -0
  294. package/src/notifications/triggers.ts +158 -0
  295. package/src/notifications/types.ts +18 -0
  296. package/src/prompt.ts +142 -0
  297. package/src/registry.ts +75 -0
  298. package/src/request-context.ts +137 -0
  299. package/src/storage/index.ts +153 -0
  300. package/dist/db/schema/rag.d.ts +0 -171
  301. package/dist/db/schema/rag.d.ts.map +0 -1
  302. package/dist/db/schema/rag.js +0 -42
  303. package/dist/db/schema/rag.js.map +0 -1
  304. package/src/db/migrations/0000_sharp_night_nurse.sql +0 -52
  305. package/src/db/migrations/0001_hesitant_vapor.sql +0 -40
  306. package/src/db/migrations/0002_numerous_lady_vermin.sql +0 -235
  307. package/src/db/migrations/0003_mighty_jean_grey.sql +0 -37
  308. package/src/db/migrations/0004_cynical_grandmaster.sql +0 -2
  309. package/src/db/migrations/0005_careful_switch.sql +0 -8
  310. package/src/db/migrations/0006_tense_blue_marvel.sql +0 -16
  311. package/src/db/migrations/0007_next_squadron_sinister.sql +0 -1
  312. package/src/db/migrations/0008_powerful_scream.sql +0 -39
  313. package/src/db/migrations/0009_sad_ink.sql +0 -1
  314. package/src/db/migrations/0010_shocking_omega_flight.sql +0 -2
  315. package/src/db/migrations/0011_ambiguous_otto_octavius.sql +0 -151
  316. package/src/db/migrations/0011_hnsw_vector_index.sql +0 -17
  317. package/src/db/migrations/0012_user_profiles.sql +0 -30
  318. package/src/db/migrations/0013_trending_seasonal_columns.sql +0 -9
  319. package/src/db/migrations/0014_add_product_type_to_agents.sql +0 -6
  320. package/src/db/migrations/0015_new_v1_tables.sql +0 -149
  321. package/src/db/migrations/0016_document_types.sql +0 -30
  322. package/src/db/migrations/0017_document_metadata.sql +0 -2
  323. package/src/db/migrations/0018_identity_graph.sql +0 -200
  324. package/src/db/migrations/0019_db_driven_prompts.sql +0 -20
  325. package/src/db/migrations/0020_pending_extractions.sql +0 -35
  326. package/src/db/migrations/0021_rag_documents.sql +0 -39
  327. package/src/db/migrations/0022_user_quotas_usage_jsonb.sql +0 -70
  328. package/src/db/migrations/0023_user_facts_user_scoped_unique.sql +0 -17
  329. package/src/db/migrations/0024_user_memories_hnsw.sql +0 -23
  330. package/src/db/migrations/0025_audit_trigger_errcode.sql +0 -21
  331. package/src/db/migrations/0026_user_profile_snapshots_composite_pk.sql +0 -17
  332. package/src/db/migrations/0027_billing_settings_mnt_columns.sql +0 -54
  333. package/src/db/migrations/0028_trial_credits_normalized_email.sql +0 -37
  334. package/src/db/migrations/0029_usage_records_conversation_idx.sql +0 -13
  335. package/src/db/migrations/0030_pending_extractions_composite_idx.sql +0 -11
  336. package/src/db/migrations/0031_rag_documents_hnsw.sql +0 -16
  337. package/src/db/migrations/0032_rate_limit_unique_idx.sql +0 -16
  338. package/src/db/migrations/0033_pending_extractions_partial_idx.sql +0 -18
  339. package/src/db/migrations/0034_rate_limit_count.sql +0 -9
  340. package/src/db/migrations/0035_credit_reservations.sql +0 -24
  341. package/src/db/migrations/0036_executions_audit_jobs.sql +0 -96
  342. package/src/db/migrations/0037_rate_limit_minute_bucket.sql +0 -46
  343. package/src/db/migrations/0038_user_quotas_generic_usage.sql +0 -25
  344. package/src/db/migrations/0039_platform_admin_impersonation.sql +0 -27
  345. package/src/db/migrations/0040_ee_audit_chain.sql +0 -23
  346. package/src/db/migrations/meta/0001_snapshot.json +0 -631
  347. package/src/db/migrations/meta/0003_snapshot.json +0 -2488
  348. package/src/db/migrations/meta/0004_snapshot.json +0 -2500
  349. package/src/db/migrations/meta/0005_snapshot.json +0 -2548
  350. package/src/db/migrations/meta/0006_snapshot.json +0 -2650
  351. package/src/db/migrations/meta/0007_snapshot.json +0 -2657
  352. package/src/db/migrations/meta/0008_snapshot.json +0 -2719
  353. package/src/db/migrations/meta/0009_snapshot.json +0 -2726
  354. package/src/db/migrations/meta/0010_snapshot.json +0 -2739
  355. package/src/db/migrations/meta/0011_snapshot.json +0 -3886
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Each trigger creates an in-app notification (awaited) and sends an email
3
+ * (fire-and-forget, errors logged). Deduplication is the caller's job.
4
+ */
5
+
6
+ import { createNotification } from "./index";
7
+ import {
8
+ sendQuotaWarningEmail,
9
+ sendTrialWarningEmail,
10
+ sendPaymentFailedEmail,
11
+ } from "../email/senders";
12
+ import { formatMoney, type Money } from "../money";
13
+
14
+ /** The locale the framework's own notification and email copy is written in. */
15
+ const COPY_LOCALE = "en";
16
+
17
+ /**
18
+ * The plan allowance is money, not tokens: `used` and `allowance` are
19
+ * amounts in the deployment's billing currency, and the copy prints
20
+ * them as such.
21
+ */
22
+ export async function triggerQuotaNotification(params: {
23
+ userId: string;
24
+ userEmail: string;
25
+ workspaceId: string;
26
+ workspaceName: string;
27
+ percentageUsed: number;
28
+ used: Money;
29
+ allowance: Money;
30
+ isExceeded: boolean;
31
+ }): Promise<void> {
32
+ const upgradeUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/settings/billing`;
33
+ const used = formatMoney(params.used, COPY_LOCALE);
34
+ const allowance = formatMoney(params.allowance, COPY_LOCALE);
35
+
36
+ await createNotification({
37
+ userId: params.userId,
38
+ workspaceId: params.workspaceId,
39
+ type: params.isExceeded ? "quota_warning_100" : "quota_warning_80",
40
+ title: params.isExceeded ? "Allowance Used Up" : "Allowance Warning",
41
+ message: params.isExceeded
42
+ ? `Your workspace "${params.workspaceName}" has used all of its ${allowance} monthly allowance. Upgrade your plan for more.`
43
+ : `Your workspace "${params.workspaceName}" has used ${used} of its ${allowance} monthly allowance (${params.percentageUsed}%).`,
44
+ metadata: {
45
+ workspaceName: params.workspaceName,
46
+ percentageUsed: params.percentageUsed,
47
+ usedMicros: params.used.amount,
48
+ allowanceMicros: params.allowance.amount,
49
+ currency: params.allowance.currency,
50
+ },
51
+ });
52
+
53
+ sendQuotaWarningEmail({
54
+ to: params.userEmail,
55
+ workspaceName: params.workspaceName,
56
+ percentageUsed: params.percentageUsed,
57
+ used: params.used,
58
+ allowance: params.allowance,
59
+ upgradeUrl,
60
+ isExceeded: params.isExceeded,
61
+ }).catch((err) =>
62
+ console.error("[Email] Failed to send quota warning:", err)
63
+ );
64
+ }
65
+
66
+ export async function triggerTrialNotification(params: {
67
+ userId: string;
68
+ userEmail: string;
69
+ workspaceId: string;
70
+ workspaceName: string;
71
+ creditsRemaining: number;
72
+ totalCredits: number;
73
+ percentageRemaining: number;
74
+ isDepleted: boolean;
75
+ }): Promise<void> {
76
+ const upgradeUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/settings/billing`;
77
+
78
+ await createNotification({
79
+ userId: params.userId,
80
+ workspaceId: params.workspaceId,
81
+ type: params.isDepleted ? "trial_depleted" : "trial_warning_20",
82
+ title: params.isDepleted
83
+ ? "Trial Credits Depleted"
84
+ : "Trial Credits Running Low",
85
+ message: params.isDepleted
86
+ ? `Your workspace "${params.workspaceName}" has used all trial credits. Subscribe to a plan to continue using AI features.`
87
+ : `Your workspace "${params.workspaceName}" has ${params.creditsRemaining.toLocaleString()} of ${params.totalCredits.toLocaleString()} trial credits remaining (${params.percentageRemaining}%).`,
88
+ metadata: {
89
+ workspaceName: params.workspaceName,
90
+ creditsRemaining: params.creditsRemaining,
91
+ totalCredits: params.totalCredits,
92
+ percentageRemaining: params.percentageRemaining,
93
+ },
94
+ });
95
+
96
+ sendTrialWarningEmail({
97
+ to: params.userEmail,
98
+ workspaceName: params.workspaceName,
99
+ creditsRemaining: params.creditsRemaining,
100
+ totalCredits: params.totalCredits,
101
+ percentageRemaining: params.percentageRemaining,
102
+ upgradeUrl,
103
+ isDepleted: params.isDepleted,
104
+ }).catch((err) =>
105
+ console.error("[Email] Failed to send trial warning:", err)
106
+ );
107
+ }
108
+
109
+ export async function triggerPaymentFailedNotification(params: {
110
+ userId: string;
111
+ userEmail: string;
112
+ workspaceId: string;
113
+ workspaceName: string;
114
+ amount: string;
115
+ }): Promise<void> {
116
+ const updatePaymentUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/settings/billing`;
117
+
118
+ await createNotification({
119
+ userId: params.userId,
120
+ workspaceId: params.workspaceId,
121
+ type: "payment_failed",
122
+ title: "Payment Failed",
123
+ message: `Payment of ${params.amount} failed for ${params.workspaceName}. Update your payment method.`,
124
+ metadata: {
125
+ workspaceName: params.workspaceName,
126
+ amount: params.amount,
127
+ },
128
+ });
129
+
130
+ sendPaymentFailedEmail({
131
+ to: params.userEmail,
132
+ workspaceName: params.workspaceName,
133
+ amount: params.amount,
134
+ updatePaymentUrl,
135
+ }).catch((err) =>
136
+ console.error("[Email] Failed to send payment failed:", err)
137
+ );
138
+ }
139
+
140
+ /** In-app only: this event sends no email. */
141
+ export async function triggerTeamMemberJoinedNotification(params: {
142
+ userId: string;
143
+ workspaceId: string;
144
+ memberName: string;
145
+ memberEmail: string;
146
+ }): Promise<void> {
147
+ await createNotification({
148
+ userId: params.userId,
149
+ workspaceId: params.workspaceId,
150
+ type: "team_member_joined",
151
+ title: "New Team Member",
152
+ message: `${params.memberName} (${params.memberEmail}) joined the workspace.`,
153
+ metadata: {
154
+ memberName: params.memberName,
155
+ memberEmail: params.memberEmail,
156
+ },
157
+ });
158
+ }
@@ -0,0 +1,18 @@
1
+ export type NotificationType =
2
+ | "quota_warning_80"
3
+ | "quota_warning_100"
4
+ | "trial_warning_20"
5
+ | "trial_depleted"
6
+ | "payment_failed"
7
+ | "team_member_joined"
8
+ | "subscription_confirmed"
9
+ | "workspace_invitation";
10
+
11
+ export interface CreateNotificationParams {
12
+ userId: string;
13
+ workspaceId?: string;
14
+ type: NotificationType;
15
+ title: string;
16
+ message: string;
17
+ metadata?: Record<string, unknown>;
18
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Prompt-injection sanitisation for user-authored text that reaches a
3
+ * system prompt (a stored summary, a profile field, a quoted web page).
4
+ * Strips role markers, override phrases and obfuscated separators, and
5
+ * reports which patterns it saw so a transport can log the attempt.
6
+ *
7
+ * Dependency-free, so any package that assembles a prompt can use it.
8
+ *
9
+ * Returns an empty string when more than half of the original was
10
+ * injection patterns: feeding the remaining fragments to the model is
11
+ * worse than dropping them.
12
+ */
13
+
14
+ const ZERO_WIDTH_CHARS =
15
+ /[\u200B\u200C\u200D\u200E\u200F\uFEFF\u00AD\u2060\u2061\u2062\u2063\u2064]/g;
16
+
17
+ function normalizeText(text: string): { withSpaces: string; stripped: string } {
18
+ return {
19
+ withSpaces: text.replace(ZERO_WIDTH_CHARS, " ").normalize("NFKC"),
20
+ stripped: text.replace(ZERO_WIDTH_CHARS, "").normalize("NFKC"),
21
+ };
22
+ }
23
+
24
+ export type InjectionPattern = { name: string; pattern: RegExp };
25
+
26
+ /**
27
+ * What is stripped. Exported so a product can extend the list for its
28
+ * own model or language without re-implementing the sweep.
29
+ */
30
+ export const INJECTION_PATTERNS: readonly InjectionPattern[] = [
31
+ { name: "[SYSTEM]", pattern: /\[SYSTEM\]/gi },
32
+ { name: "<<SYS>>", pattern: /<<SYS>>/gi },
33
+ { name: "<|system|>", pattern: /<\|system\|>/gi },
34
+ { name: "<|im_start|>", pattern: /<\|im_start\|>/gi },
35
+ { name: "<|im_end|>", pattern: /<\|im_end\|>/gi },
36
+ { name: "system: prefix", pattern: /^(SYSTEM|System|system)\s*:\s*/gm },
37
+ {
38
+ name: "ignore previous instructions",
39
+ pattern: /ignore\s+(?:all\s+)?previous\s+instructions?/gi,
40
+ },
41
+ { name: "disregard above", pattern: /disregard\s+(?:everything\s+)?above/gi },
42
+ { name: "forget everything", pattern: /forget\s+(?:all|everything)/gi },
43
+ {
44
+ name: "override instructions",
45
+ pattern: /override\s+(?:your\s+)?instructions?/gi,
46
+ },
47
+ {
48
+ name: "new instructions",
49
+ pattern: /(?:your\s+)?new\s+instructions?\s+(?:are|follow)/gi,
50
+ },
51
+ { name: "you are now", pattern: /you\s+are\s+now\s+/gi },
52
+ { name: "act as", pattern: /\bact\s+as\s+(?:a\s+|an\s+)?/gi },
53
+ { name: "pretend to be", pattern: /pretend\s+to\s+be\s+/gi },
54
+ { name: "roleplay as", pattern: /roleplay\s+as\s+/gi },
55
+ { name: "play the role", pattern: /play\s+(?:the\s+)?role\s+of\s+/gi },
56
+ { name: "separator ---", pattern: /\n{0,2}-{3,}\n{0,2}/g },
57
+ { name: "separator ===", pattern: /\n{0,2}={3,}\n{0,2}/g },
58
+ { name: "separator ###", pattern: /\n{0,2}#{3,}\n{0,2}/g },
59
+ { name: "script tag", pattern: /<script[\s>]/gi },
60
+ { name: "base64 data", pattern: /data:[^;]+;base64,/gi },
61
+ {
62
+ name: "do not follow",
63
+ pattern: /do\s+not\s+follow\s+(?:your\s+)?(?:previous\s+)?instructions?/gi,
64
+ },
65
+ {
66
+ name: "new system prompt",
67
+ pattern: /(?:new|updated?)\s+system\s+prompt/gi,
68
+ },
69
+ ];
70
+
71
+ export type InjectionReport = {
72
+ isInjection: boolean;
73
+ /** Names of the patterns that matched, for a log line. */
74
+ patterns: string[];
75
+ };
76
+
77
+ /**
78
+ * Which patterns the text carries, without changing it. Zero-width
79
+ * characters are both removed and replaced by spaces before matching,
80
+ * so neither obfuscation hides a marker.
81
+ */
82
+ export function detectPromptInjection(
83
+ text: string,
84
+ patterns: readonly InjectionPattern[] = INJECTION_PATTERNS
85
+ ): InjectionReport {
86
+ // Stryker disable next-line all: a fast path, not behaviour. Empty or
87
+ // blank text matches no pattern, so every mutant of this guard —
88
+ // dropping it, inverting it, swapping the operator — returns the same
89
+ // report the loop below would have built.
90
+ if (!text || text.trim().length === 0) {
91
+ return { isInjection: false, patterns: [] };
92
+ }
93
+ const { withSpaces, stripped } = normalizeText(text);
94
+ const matched: string[] = [];
95
+ for (const { name, pattern } of patterns) {
96
+ // Fresh instances: a global RegExp carries lastIndex between tests.
97
+ const a = new RegExp(pattern.source, pattern.flags);
98
+ const b = new RegExp(pattern.source, pattern.flags);
99
+ if (a.test(withSpaces) || b.test(stripped)) matched.push(name);
100
+ }
101
+ return { isInjection: matched.length > 0, patterns: matched };
102
+ }
103
+
104
+ /**
105
+ * The text with every pattern replaced by a space and whitespace
106
+ * collapsed — or an empty string when the text was mostly patterns.
107
+ */
108
+ export function sanitizeForSystemPrompt(
109
+ text: string,
110
+ patterns: readonly InjectionPattern[] = INJECTION_PATTERNS
111
+ ): string {
112
+ if (!text || text.trim().length === 0) return text;
113
+
114
+ const originalLength = text.length;
115
+ const { withSpaces, stripped } = normalizeText(text);
116
+ // Detection reads both forms; so does this. A marker split by a
117
+ // zero-width character ("ig\u200Bnore") only exists once the
118
+ // character is removed, and cleaning the spaced form would pass it on.
119
+ const hiddenByZeroWidth = patterns.some(
120
+ ({ pattern }) =>
121
+ new RegExp(pattern.source, pattern.flags).test(stripped) &&
122
+ !new RegExp(pattern.source, pattern.flags).test(withSpaces)
123
+ );
124
+ let sanitized = hiddenByZeroWidth ? stripped : withSpaces;
125
+ for (const { pattern } of patterns) {
126
+ sanitized = sanitized.replace(
127
+ new RegExp(pattern.source, pattern.flags),
128
+ " "
129
+ );
130
+ }
131
+ sanitized = sanitized
132
+ // A pattern replaced between two newlines leaves a whitespace-only
133
+ // line behind; drop those before collapsing blank lines.
134
+ .replace(/\n[ \t]+\n/g, "\n\n")
135
+ .replace(/\n{3,}/g, "\n\n")
136
+ .replace(/ {2,}/g, " ")
137
+ .trim();
138
+
139
+ const removed = originalLength - sanitized.length;
140
+ if (removed / originalLength > 0.5) return "";
141
+ return sanitized;
142
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Registries that survive a bundler duplicating the module they live in.
3
+ *
4
+ * Next.js's server build can instantiate a package twice, so a module-scope
5
+ * `Map` written by the composition root may not be the one a page reads —
6
+ * and the failure is quiet (registries look empty, checks fall through to
7
+ * their defaults). Keying storage off `Symbol.for()` puts the map in the
8
+ * realm's global symbol registry, which every copy of the module shares.
9
+ *
10
+ * `globalThis` is per process: Node and Edge runtimes each still need the
11
+ * composition root to run.
12
+ */
13
+
14
+ /** What the module stores behind its global symbol. */
15
+ type Slot<T> = {
16
+ readonly map: Map<string, T>;
17
+ /** The module URL of the copy that created it, for the warning below. */
18
+ readonly origin: string;
19
+ };
20
+
21
+ const seen = new Set<string>();
22
+
23
+ /**
24
+ * A `Map` shared by every copy of the module that asks for the same key.
25
+ * Use it wherever a registry is written by the composition root and read by
26
+ * request-time code:
27
+ *
28
+ * const plans = createRegistry<PlanMap>("billing/plans");
29
+ *
30
+ * `key` must be unique across the framework (`"<package>/<registry>"`). The
31
+ * value type is not checked across copies.
32
+ */
33
+ export function createRegistry<T>(key: string): Map<string, T> {
34
+ const symbol = Symbol.for(`@intelligo-dev/registry/${key}`);
35
+ const globals = globalThis as unknown as Record<symbol, Slot<T> | undefined>;
36
+
37
+ const existing = globals[symbol];
38
+ if (existing) {
39
+ // A second copy reaching the same slot is not an error. Warn once per
40
+ // key anyway: every other module-scope value in that file is
41
+ // duplicated too, and those fail silently.
42
+ if (existing.origin !== import.meta.url && !seen.has(key)) {
43
+ seen.add(key);
44
+ console.warn(
45
+ `[intelligo] registry "${key}" is being read from a second module ` +
46
+ `instance (${import.meta.url}, first seen from ${existing.origin}). ` +
47
+ `The registry itself is shared, so this is safe — but any other ` +
48
+ `module-scope state in that file is now duplicated.`
49
+ );
50
+ }
51
+ return existing.map;
52
+ }
53
+
54
+ const map = new Map<string, T>();
55
+ globals[symbol] = { map, origin: import.meta.url };
56
+ return map;
57
+ }
58
+
59
+ /**
60
+ * A single shared value, for registries that are not maps: a module-scope
61
+ * `let` has the same duplication problem a `Map` does.
62
+ */
63
+ export function createRegistryRef<T>(
64
+ key: string,
65
+ initial: T
66
+ ): { get(): T; set(value: T): void } {
67
+ const box = createRegistry<T>(`ref/${key}`);
68
+ if (!box.has("value")) box.set("value", initial);
69
+ return {
70
+ get: () => box.get("value") as T,
71
+ set: (value: T) => {
72
+ box.set("value", value);
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Where request-scoped context comes from. The framework asks for headers
3
+ * through `getRequestHeaders()`; the application binds the source once,
4
+ * from its composition root, so no package outside `@intelligo-dev/next`
5
+ * depends on Next:
6
+ *
7
+ * import { setRequestContextSource } from "@intelligo-dev/core/request-context";
8
+ * import { nextRequestContext } from "@intelligo-dev/next";
9
+ *
10
+ * setRequestContextSource(nextRequestContext);
11
+ *
12
+ * An unbound source throws rather than guessing at the request, which is how
13
+ * a session gets read from the wrong one.
14
+ */
15
+
16
+ import { createRegistryRef } from "./registry";
17
+
18
+ /**
19
+ * Produces the current request's headers.
20
+ *
21
+ * Async because Next's accessor is, and sync sources satisfy it too —
22
+ * `getRequestHeaders` awaits either.
23
+ */
24
+ export type RequestContextSource = () => Headers | Promise<Headers>;
25
+
26
+ export class RequestContextUnavailableError extends Error {
27
+ readonly code = "request_context_unavailable";
28
+ constructor() {
29
+ super(
30
+ "No request context source is bound. Call setRequestContextSource() " +
31
+ "from your composition root — `nextRequestContext` from " +
32
+ "@intelligo-dev/next in a Next.js app — or wrap the call in " +
33
+ "withRequestHeaders() outside a request."
34
+ );
35
+ this.name = "RequestContextUnavailableError";
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Held in the cross-instance registry, not a module variable: a bundler that
41
+ * duplicates this module would otherwise give the composition root one copy
42
+ * and the request path another.
43
+ */
44
+ const source = createRegistryRef<RequestContextSource | undefined>(
45
+ "core/request-context-source",
46
+ undefined
47
+ );
48
+
49
+ /**
50
+ * Headers bound by `withRequestHeaders`, visible only to the async work
51
+ * that call started. Scoped rather than a shared slot: a slot would hand
52
+ * one caller's cookie to every request that reads it while the call is
53
+ * awaiting.
54
+ */
55
+ type HeaderScope = {
56
+ run<T>(headers: Headers, fn: () => T): T;
57
+ getStore(): Headers | undefined;
58
+ };
59
+
60
+ const scope = createRegistryRef<HeaderScope | undefined>(
61
+ "core/request-headers-scope",
62
+ undefined
63
+ );
64
+
65
+ /**
66
+ * The runtime's `AsyncLocalStorage`: the global one Next.js and edge
67
+ * runtimes provide, or Node's own, loaded only when there is none — a
68
+ * script, a worker or a test.
69
+ */
70
+ async function headerScope(): Promise<HeaderScope> {
71
+ const existing = scope.get();
72
+ if (existing) return existing;
73
+ const Storage =
74
+ (globalThis as { AsyncLocalStorage?: new () => HeaderScope })
75
+ .AsyncLocalStorage ??
76
+ (await import("node:async_hooks")).AsyncLocalStorage;
77
+ const created = new Storage() as HeaderScope;
78
+ scope.set(created);
79
+ return created;
80
+ }
81
+
82
+ export function setRequestContextSource(next: RequestContextSource): void {
83
+ source.set(next);
84
+ }
85
+
86
+ /** Forget the bound source. For tests composing a fresh root. */
87
+ export function clearRequestContextSource(): void {
88
+ source.set(undefined);
89
+ }
90
+
91
+ export function hasRequestContextSource(): boolean {
92
+ return source.get() !== undefined || scope.get()?.getStore() !== undefined;
93
+ }
94
+
95
+ /**
96
+ * The current request's headers.
97
+ *
98
+ * @throws {RequestContextUnavailableError} when nothing is bound.
99
+ */
100
+ export async function getRequestHeaders(): Promise<Headers> {
101
+ const explicit = scope.get()?.getStore();
102
+ if (explicit) return explicit;
103
+
104
+ const resolve = source.get();
105
+ if (!resolve) throw new RequestContextUnavailableError();
106
+ return await resolve();
107
+ }
108
+
109
+ /**
110
+ * Validates a client-sent time zone. The value comes from the browser and
111
+ * reaches SQL (`AT TIME ZONE`) and `Intl`, so anything this runtime does not
112
+ * know as a zone, or no value at all, becomes `UTC`.
113
+ */
114
+ export function resolveTimeZone(value: string | null | undefined): string {
115
+ if (!value) return "UTC";
116
+ try {
117
+ // Throws RangeError for anything that is not a zone it knows.
118
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
119
+ return value;
120
+ } catch {
121
+ return "UTC";
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Runs `fn` with these headers, whatever the ambient source says — for a
127
+ * background job, a script acting as a user, or an integration test.
128
+ * Only `fn` and the work it starts see them; a concurrent request keeps
129
+ * its own. Nesting works: the innermost call wins.
130
+ */
131
+ export async function withRequestHeaders<T>(
132
+ headers: Headers,
133
+ fn: () => Promise<T> | T
134
+ ): Promise<T> {
135
+ const storage = await headerScope();
136
+ return await storage.run(headers, async () => await fn());
137
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Where files live. The framework stores attachment rows and hands out URLs;
3
+ * the application binds one adapter (S3, R2, Vercel Blob, disk) from its
4
+ * composition root:
5
+ *
6
+ * import { setStorageAdapter } from "@intelligo-dev/core/storage";
7
+ * setStorageAdapter(createS3Storage({ bucket, region })); // the app's lib/storage.ts
8
+ *
9
+ * An unbound adapter throws where a file is needed. Keys carry the tenant —
10
+ * `ws/<workspaceId>/att/<id>` — so a signed URL for one workspace's file can
11
+ * never be minted from another's row.
12
+ */
13
+
14
+ import { createRegistryRef } from "../registry";
15
+
16
+ export type StorageBody = Uint8Array | Blob | ReadableStream<Uint8Array>;
17
+
18
+ export type StorageObject = {
19
+ key: string;
20
+ contentType: string;
21
+ size?: number;
22
+ };
23
+
24
+ export type SignedUrlOptions = {
25
+ /** Default 900 (fifteen minutes). */
26
+ expiresInSeconds?: number;
27
+ disposition?: "inline" | "attachment";
28
+ /** Sent as the download's file name when the adapter supports it. */
29
+ filename?: string;
30
+ };
31
+
32
+ export interface StorageAdapter {
33
+ put(input: {
34
+ key: string;
35
+ body: StorageBody;
36
+ contentType: string;
37
+ contentLength?: number;
38
+ }): Promise<StorageObject>;
39
+ /** A URL the browser — or a model provider — can fetch for a while. */
40
+ getSignedUrl(key: string, options?: SignedUrlOptions): Promise<string>;
41
+ /** The bytes, for text extraction or a streaming proxy. Optional: not every store can. */
42
+ get?(key: string): Promise<{
43
+ body: ReadableStream<Uint8Array>;
44
+ contentType: string;
45
+ size?: number;
46
+ }>;
47
+ delete(key: string): Promise<void>;
48
+ }
49
+
50
+ export class StorageUnavailableError extends Error {
51
+ readonly code = "storage_unavailable";
52
+ constructor() {
53
+ super(
54
+ "No storage adapter is bound. Call setStorageAdapter() from your " +
55
+ "composition root with your bucket's adapter (S3, R2, Blob), or " +
56
+ "createMemoryStorage() for tests."
57
+ );
58
+ this.name = "StorageUnavailableError";
59
+ }
60
+ }
61
+
62
+ const adapter = createRegistryRef<StorageAdapter | undefined>(
63
+ "core/storage-adapter",
64
+ undefined
65
+ );
66
+
67
+ export function setStorageAdapter(next: StorageAdapter): void {
68
+ adapter.set(next);
69
+ }
70
+
71
+ /** Forget the bound adapter. For tests composing a fresh root. */
72
+ export function clearStorageAdapter(): void {
73
+ adapter.set(undefined);
74
+ }
75
+
76
+ export function hasStorageAdapter(): boolean {
77
+ return adapter.get() !== undefined;
78
+ }
79
+
80
+ /** @throws {StorageUnavailableError} when nothing is bound. */
81
+ export function getStorageAdapter(): StorageAdapter {
82
+ const bound = adapter.get();
83
+ if (!bound) throw new StorageUnavailableError();
84
+ return bound;
85
+ }
86
+
87
+ /** The key a chat attachment is stored under. */
88
+ export function attachmentStorageKey(workspaceId: string, id: string): string {
89
+ return `ws/${workspaceId}/att/${id}`;
90
+ }
91
+
92
+ async function toBytes(body: StorageBody): Promise<Uint8Array> {
93
+ if (body instanceof Uint8Array) return body;
94
+ if (body instanceof Blob) return new Uint8Array(await body.arrayBuffer());
95
+ const chunks: Uint8Array[] = [];
96
+ const reader = body.getReader();
97
+ for (;;) {
98
+ const { done, value } = await reader.read();
99
+ if (done) break;
100
+ chunks.push(value);
101
+ }
102
+ const size = chunks.reduce((n, c) => n + c.byteLength, 0);
103
+ const out = new Uint8Array(size);
104
+ let offset = 0;
105
+ for (const chunk of chunks) {
106
+ out.set(chunk, offset);
107
+ offset += chunk.byteLength;
108
+ }
109
+ return out;
110
+ }
111
+
112
+ function toBase64(bytes: Uint8Array): string {
113
+ if (typeof Buffer !== "undefined")
114
+ return Buffer.from(bytes).toString("base64");
115
+ let binary = "";
116
+ for (const byte of bytes) binary += String.fromCharCode(byte);
117
+ return btoa(binary);
118
+ }
119
+
120
+ /**
121
+ * Files in memory, "signed" as data URLs, for tests and apps without a
122
+ * bucket. Nothing survives a restart.
123
+ */
124
+ export function createMemoryStorage(): StorageAdapter & {
125
+ readonly objects: Map<string, { bytes: Uint8Array; contentType: string }>;
126
+ } {
127
+ const objects = new Map<string, { bytes: Uint8Array; contentType: string }>();
128
+ return {
129
+ objects,
130
+ async put({ key, body, contentType }) {
131
+ const bytes = await toBytes(body);
132
+ objects.set(key, { bytes, contentType });
133
+ return { key, contentType, size: bytes.byteLength };
134
+ },
135
+ async getSignedUrl(key) {
136
+ const object = objects.get(key);
137
+ if (!object) throw new Error(`No object at ${key}`);
138
+ return `data:${object.contentType};base64,${toBase64(object.bytes)}`;
139
+ },
140
+ async get(key) {
141
+ const object = objects.get(key);
142
+ if (!object) throw new Error(`No object at ${key}`);
143
+ return {
144
+ body: new Blob([object.bytes as BlobPart]).stream(),
145
+ contentType: object.contentType,
146
+ size: object.bytes.byteLength,
147
+ };
148
+ },
149
+ async delete(key) {
150
+ objects.delete(key);
151
+ },
152
+ };
153
+ }