@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
package/src/logger.ts ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Structured pino logger. Emails and ids are redacted in production and
3
+ * passed through in development; credential fields are always redacted.
4
+ *
5
+ * ```typescript
6
+ * import { createLogger } from "@intelligo-dev/core/logger";
7
+ * const log = createLogger("MyModule");
8
+ * log.info("User created", { email: "user@example.com" });
9
+ * // Production: {"level":30,"module":"MyModule","msg":"User created","email":"u***@example.com"}
10
+ * // Development: {"level":30,"module":"MyModule","msg":"User created","email":"user@example.com"}
11
+ * ```
12
+ */
13
+
14
+ import pino from "pino";
15
+
16
+ const isProduction = process.env.NODE_ENV === "production";
17
+
18
+ /**
19
+ * Redact email addresses in production.
20
+ * user@example.com -> u***@example.com
21
+ */
22
+ function redactEmail(email: string): string {
23
+ if (!isProduction) return email;
24
+ const [local, domain] = email.split("@");
25
+ if (!local || !domain) return "***@***";
26
+ return `${local[0]}***@${domain}`;
27
+ }
28
+
29
+ /**
30
+ * Redact IDs in production (workspace IDs, UUIDs).
31
+ * ws_abc123def456 -> ws_a...f456
32
+ * 550e8400-e29b-41d4-a716-446655440000 -> 550e...0000
33
+ */
34
+ function redactId(id: string): string {
35
+ if (!isProduction) return id;
36
+ if (id.length <= 8) return id;
37
+ return `${id.slice(0, 4)}...${id.slice(-4)}`;
38
+ }
39
+
40
+ /**
41
+ * Field name fragments whose values are always replaced with [REDACTED], in
42
+ * every environment. Exported so an error reporter's scrubber can share the
43
+ * list.
44
+ */
45
+ export const CREDENTIAL_KEY_FRAGMENTS = [
46
+ "password",
47
+ "passwd",
48
+ "secret",
49
+ "token",
50
+ "apikey",
51
+ "api_key",
52
+ "authorization",
53
+ "auth_header",
54
+ "cookie",
55
+ "session_token",
56
+ "session_secret",
57
+ "credit_card",
58
+ "creditcard",
59
+ "card_number",
60
+ "cvv",
61
+ ] as const;
62
+
63
+ /** True when `key` contains any credential fragment (case-insensitive substring). */
64
+ export function isCredentialKey(key: string): boolean {
65
+ const lower = key.toLowerCase();
66
+ return CREDENTIAL_KEY_FRAGMENTS.some((needle) => lower.includes(needle));
67
+ }
68
+
69
+ /**
70
+ * An identifier's key: `id`, `userId`, `workspace_id`. Matched as a
71
+ * suffix, not a substring, so `provider` or `paid` keep their values.
72
+ */
73
+ function isIdKey(key: string): boolean {
74
+ return /^id$|[a-z0-9]Id$|[_-]id$/.test(key) || /workspace/i.test(key);
75
+ }
76
+
77
+ /** Nested objects and arrays are walked, to this depth, so a credential inside one is still found. */
78
+ const MAX_REDACT_DEPTH = 6;
79
+
80
+ function redactValue(key: string, value: unknown, depth: number): unknown {
81
+ if (isCredentialKey(key)) return "[REDACTED]";
82
+
83
+ if (value !== null && typeof value === "object") {
84
+ if (depth >= MAX_REDACT_DEPTH) return "[TRUNCATED]";
85
+ if (Array.isArray(value)) {
86
+ return value.map((item) => redactValue(key, item, depth + 1));
87
+ }
88
+ if (Object.getPrototypeOf(value) === Object.prototype) {
89
+ return redactObject(value as Record<string, unknown>, depth + 1);
90
+ }
91
+ return value;
92
+ }
93
+
94
+ // Email and id redaction is production-only, so development logs show
95
+ // real values.
96
+ if (!isProduction || typeof value !== "string") return value;
97
+ if (key.toLowerCase().includes("email")) return redactEmail(value);
98
+ if (isIdKey(key)) return redactId(value);
99
+ return value;
100
+ }
101
+
102
+ function redactObject(
103
+ data: Record<string, unknown>,
104
+ depth: number
105
+ ): Record<string, unknown> {
106
+ const redacted: Record<string, unknown> = {};
107
+ for (const [key, value] of Object.entries(data)) {
108
+ redacted[key] = redactValue(key, value, depth);
109
+ }
110
+ return redacted;
111
+ }
112
+
113
+ function redactData(data: Record<string, unknown>): Record<string, unknown> {
114
+ return redactObject(data, 0);
115
+ }
116
+
117
+ const baseLogger = pino({
118
+ level: isProduction ? "info" : "debug",
119
+ formatters: {
120
+ level(label) {
121
+ return { level: label };
122
+ },
123
+ },
124
+ timestamp: isProduction ? pino.stdTimeFunctions.isoTime : false,
125
+ });
126
+
127
+ class Logger {
128
+ private pino: pino.Logger;
129
+
130
+ constructor(context: string) {
131
+ this.pino = baseLogger.child({ module: context });
132
+ }
133
+
134
+ info(message: string, data?: Record<string, unknown>) {
135
+ if (data) {
136
+ this.pino.info(redactData(data), message);
137
+ } else {
138
+ this.pino.info(message);
139
+ }
140
+ }
141
+
142
+ warn(message: string, data?: Record<string, unknown>) {
143
+ if (data) {
144
+ this.pino.warn(redactData(data), message);
145
+ } else {
146
+ this.pino.warn(message);
147
+ }
148
+ }
149
+
150
+ error(message: string, data?: Record<string, unknown>) {
151
+ if (data) {
152
+ this.pino.error(redactData(data), message);
153
+ } else {
154
+ this.pino.error(message);
155
+ }
156
+ }
157
+
158
+ debug(message: string, data?: Record<string, unknown>) {
159
+ if (data) {
160
+ this.pino.debug(redactData(data), message);
161
+ } else {
162
+ this.pino.debug(message);
163
+ }
164
+ }
165
+ }
166
+
167
+ export function createLogger(context: string): Logger {
168
+ return new Logger(context);
169
+ }
170
+
171
+ /**
172
+ * A logger writing to a custom pino destination, for capturing output in
173
+ * tests.
174
+ * @internal
175
+ */
176
+ export function _createLoggerWithStream(
177
+ context: string,
178
+ stream: pino.DestinationStream
179
+ ): Logger {
180
+ const customBase = pino(
181
+ {
182
+ level: isProduction ? "info" : "debug",
183
+ formatters: {
184
+ level(label) {
185
+ return { level: label };
186
+ },
187
+ },
188
+ timestamp: false,
189
+ },
190
+ stream
191
+ );
192
+ const instance = new Logger(context);
193
+ (instance as any).pino = customBase.child({ module: context });
194
+ return instance;
195
+ }
196
+
197
+ export { Logger };
198
+ export const logger = createLogger("app");
package/src/money.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Money, in one unit, with the currency attached. Imports nothing:
3
+ * `@intelligo-dev/executions/pricing` is a zero-import leaf that client
4
+ * bundles reach, and it needs these types.
5
+ *
6
+ * **Micros, not minor units.** A chat turn on a cheap model costs a
7
+ * fraction of a cent; rounding it to whole cents overcharges by more than a
8
+ * third on every request.
9
+ *
10
+ * **Currency on the amount, not in a global.** A bare `number` lets one
11
+ * unit be sold as another; here the currency travels with the amount and
12
+ * `add` refuses to mix two.
13
+ */
14
+
15
+ declare const isoTag: unique symbol;
16
+ declare const microsTag: unique symbol;
17
+
18
+ /** An ISO-4217 alphabetic code, upper-cased and validated. */
19
+ export type CurrencyCode = string & { readonly [isoTag]: true };
20
+
21
+ /** An integer number of millionths of one major currency unit. */
22
+ export type Micros = number & { readonly [microsTag]: true };
23
+
24
+ /** An amount and the currency it is denominated in. */
25
+ export type Money = {
26
+ readonly amount: Micros;
27
+ readonly currency: CurrencyCode;
28
+ };
29
+
30
+ /** Thrown for every misuse in this module, so callers can catch one type. */
31
+ export class MoneyError extends Error {
32
+ readonly code: string;
33
+ constructor(code: string, message: string) {
34
+ super(message);
35
+ this.code = code;
36
+ this.name = "MoneyError";
37
+ }
38
+ }
39
+
40
+ const MICROS_PER_UNIT = 1_000_000;
41
+
42
+ /**
43
+ * Currencies whose minor unit is not the usual hundredth; everything else
44
+ * is 2. Getting this wrong charges ¥100,000 instead of ¥1,000, because
45
+ * Stripe takes zero-decimal currencies in whole units.
46
+ */
47
+ const MINOR_EXPONENT: Readonly<Record<string, 0 | 2 | 3>> = {
48
+ BIF: 0,
49
+ CLP: 0,
50
+ DJF: 0,
51
+ GNF: 0,
52
+ ISK: 0,
53
+ JPY: 0,
54
+ KMF: 0,
55
+ KRW: 0,
56
+ MGA: 0,
57
+ MNT: 0,
58
+ PYG: 0,
59
+ RWF: 0,
60
+ UGX: 0,
61
+ UYI: 0,
62
+ VND: 0,
63
+ VUV: 0,
64
+ XAF: 0,
65
+ XOF: 0,
66
+ XPF: 0,
67
+ BHD: 3,
68
+ IQD: 3,
69
+ JOD: 3,
70
+ KWD: 3,
71
+ LYD: 3,
72
+ OMR: 3,
73
+ TND: 3,
74
+ };
75
+
76
+ /**
77
+ * Validates and normalises an ISO-4217 code. Shape only: a list would go
78
+ * stale and block a deployment selling in a code it does not know.
79
+ * `minorExponent` gives an unknown code its default.
80
+ */
81
+ export function currency(code: string): CurrencyCode {
82
+ const upper = code.toUpperCase();
83
+ if (!/^[A-Z]{3}$/.test(upper)) {
84
+ throw new MoneyError(
85
+ "invalid_currency",
86
+ `"${code}" is not an ISO-4217 alphabetic code (three letters).`
87
+ );
88
+ }
89
+ return upper as CurrencyCode;
90
+ }
91
+
92
+ /**
93
+ * An integer count of micros, up to `Number.MAX_SAFE_INTEGER` (about 9
94
+ * billion major units) — low enough to catch a value in the wrong unit.
95
+ */
96
+ export function micros(value: number): Micros {
97
+ if (!Number.isInteger(value)) {
98
+ throw new MoneyError(
99
+ "not_an_integer",
100
+ `Micros must be a whole number; received ${value}. Convert with fromMajor/fromMinor rather than multiplying by hand.`
101
+ );
102
+ }
103
+ if (!Number.isSafeInteger(value)) {
104
+ throw new MoneyError(
105
+ "out_of_range",
106
+ `${value} micros exceeds the safe integer range.`
107
+ );
108
+ }
109
+ return value as Micros;
110
+ }
111
+
112
+ export function money(amount: number, code: string | CurrencyCode): Money {
113
+ return { amount: micros(amount), currency: currency(code) };
114
+ }
115
+
116
+ /** How many decimal places this currency's minor unit has. */
117
+ export function minorExponent(code: CurrencyCode): 0 | 2 | 3 {
118
+ return MINOR_EXPONENT[code] ?? 2;
119
+ }
120
+
121
+ /** `12.34` USD → `12_340_000` micros. */
122
+ export function fromMajor(value: number, code: string | CurrencyCode): Money {
123
+ return {
124
+ amount: micros(Math.round(value * MICROS_PER_UNIT)),
125
+ currency: currency(code),
126
+ };
127
+ }
128
+
129
+ /** `1234` cents → `12_340_000` micros. */
130
+ export function fromMinor(value: number, code: string | CurrencyCode): Money {
131
+ const c = currency(code);
132
+ const scale = MICROS_PER_UNIT / 10 ** minorExponent(c);
133
+ return { amount: micros(Math.round(value * scale)), currency: c };
134
+ }
135
+
136
+ /**
137
+ * The integer a payment provider wants: whole units for a zero-decimal
138
+ * currency, hundredths for most, thousandths for a few.
139
+ *
140
+ * Rounds up. A fraction of a minor unit is money the deployment has
141
+ * already spent, and rounding it away means eating the difference on
142
+ * every transaction.
143
+ */
144
+ export function toMinor(value: Money): number {
145
+ const scale = MICROS_PER_UNIT / 10 ** minorExponent(value.currency);
146
+ return Math.ceil(value.amount / scale);
147
+ }
148
+
149
+ /** The amount as a plain decimal number, for display and for charts. */
150
+ export function toMajor(value: Money): number {
151
+ return value.amount / MICROS_PER_UNIT;
152
+ }
153
+
154
+ export const zero = (code: string | CurrencyCode): Money => ({
155
+ amount: 0 as Micros,
156
+ currency: currency(code),
157
+ });
158
+
159
+ function sameCurrency(a: Money, b: Money, op: string): void {
160
+ if (a.currency !== b.currency) {
161
+ throw new MoneyError(
162
+ "currency_mismatch",
163
+ `Cannot ${op} ${a.currency} and ${b.currency}. Convert one first — an exchange rate is a decision, not a coercion.`
164
+ );
165
+ }
166
+ }
167
+
168
+ export function add(a: Money, b: Money): Money {
169
+ sameCurrency(a, b, "add");
170
+ return { amount: micros(a.amount + b.amount), currency: a.currency };
171
+ }
172
+
173
+ export function subtract(a: Money, b: Money): Money {
174
+ sameCurrency(a, b, "subtract");
175
+ return { amount: micros(a.amount - b.amount), currency: a.currency };
176
+ }
177
+
178
+ /** Scale by a plain factor — a margin multiplier, a percentage, a count. */
179
+ export function multiply(value: Money, factor: number): Money {
180
+ if (!Number.isFinite(factor)) {
181
+ throw new MoneyError("invalid_factor", `Factor must be finite.`);
182
+ }
183
+ return {
184
+ amount: micros(Math.ceil(value.amount * factor)),
185
+ currency: value.currency,
186
+ };
187
+ }
188
+
189
+ export function compare(a: Money, b: Money): number {
190
+ sameCurrency(a, b, "compare");
191
+ return a.amount === b.amount ? 0 : a.amount < b.amount ? -1 : 1;
192
+ }
193
+
194
+ export const isZero = (value: Money): boolean => value.amount === 0;
195
+ export const isNegative = (value: Money): boolean => value.amount < 0;
196
+
197
+ /**
198
+ * Convert at an explicit rate, expressed in micros so the rate itself
199
+ * carries no float error: `3_450_000_000` is 3450.0.
200
+ *
201
+ * There is no ambient rate and no default. A framework that guesses an
202
+ * exchange rate is inventing money.
203
+ */
204
+ export function convert(
205
+ value: Money,
206
+ to: string | CurrencyCode,
207
+ rateMicros: number
208
+ ): Money {
209
+ const target = currency(to);
210
+ if (!Number.isInteger(rateMicros) || rateMicros <= 0) {
211
+ throw new MoneyError(
212
+ "invalid_rate",
213
+ `Rate must be a positive integer number of micros; received ${rateMicros}.`
214
+ );
215
+ }
216
+ if (target === value.currency) {
217
+ if (rateMicros !== MICROS_PER_UNIT) {
218
+ throw new MoneyError(
219
+ "invalid_rate",
220
+ `Converting ${value.currency} to itself at ${rateMicros / MICROS_PER_UNIT} is not a conversion.`
221
+ );
222
+ }
223
+ return value;
224
+ }
225
+ return {
226
+ amount: micros(Math.ceil((value.amount * rateMicros) / MICROS_PER_UNIT)),
227
+ currency: target,
228
+ };
229
+ }
230
+
231
+ /** Two significant digits is enough to read a sub-cent amount by. */
232
+ const SIGNIFICANT_DIGITS = 2;
233
+
234
+ /**
235
+ * How many decimals an amount needs to be legible.
236
+ *
237
+ * The currency's own minor unit, except when that would render a real
238
+ * charge as nothing: one chat turn costs a fraction of a cent, and a
239
+ * usage page that says every request cost `$0.00` is worse than one
240
+ * that says `$0.0048`. Below the minor unit the amount is shown to two
241
+ * significant digits, capped at the six decimals micros can hold.
242
+ */
243
+ export function displayFractionDigits(value: Money): number {
244
+ const digits = minorExponent(value.currency);
245
+ const major = Math.abs(toMajor(value));
246
+ // Zero, and anything that still rounds to a visible minor unit, reads
247
+ // the way the currency is normally written.
248
+ if (major === 0 || major >= 10 ** -digits / 2) return digits;
249
+ const leadingZeros = Math.floor(-Math.log10(major));
250
+ return Math.min(6, leadingZeros + SIGNIFICANT_DIGITS);
251
+ }
252
+
253
+ /**
254
+ * Formats for a human, in their locale. `Intl` owns the symbol, its
255
+ * position and the separators.
256
+ */
257
+ export function formatMoney(
258
+ value: Money,
259
+ locale: string,
260
+ options: Intl.NumberFormatOptions = {}
261
+ ): string {
262
+ const digits = displayFractionDigits(value);
263
+ return new Intl.NumberFormat(locale, {
264
+ style: "currency",
265
+ currency: value.currency,
266
+ minimumFractionDigits: digits,
267
+ maximumFractionDigits: digits,
268
+ ...options,
269
+ }).format(toMajor(value));
270
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * User notifications. User-scoped, not workspace-scoped, so a user sees
3
+ * their notifications from every workspace.
4
+ */
5
+
6
+ import { db } from "../db";
7
+ import { notifications } from "../db/schema";
8
+ import { eq, and, desc, count } from "drizzle-orm";
9
+ import type { CreateNotificationParams } from "./types";
10
+
11
+ export type { NotificationType, CreateNotificationParams } from "./types";
12
+
13
+ export {
14
+ triggerQuotaNotification,
15
+ triggerTrialNotification,
16
+ triggerPaymentFailedNotification,
17
+ triggerTeamMemberJoinedNotification,
18
+ } from "./triggers";
19
+
20
+ /** Creates a notification; `metadata` is stored as a JSON string. */
21
+ export async function createNotification(params: CreateNotificationParams) {
22
+ const [notification] = await db
23
+ .insert(notifications)
24
+ .values({
25
+ userId: params.userId,
26
+ workspaceId: params.workspaceId,
27
+ type: params.type,
28
+ title: params.title,
29
+ message: params.message,
30
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
31
+ })
32
+ .returning();
33
+
34
+ return notification;
35
+ }
36
+
37
+ /** The user's unread notifications, newest first. */
38
+ export async function getUnreadNotifications(
39
+ userId: string,
40
+ limit: number = 20
41
+ ) {
42
+ const rows = await db
43
+ .select()
44
+ .from(notifications)
45
+ .where(
46
+ and(eq(notifications.userId, userId), eq(notifications.isRead, false))
47
+ )
48
+ .orderBy(desc(notifications.createdAt))
49
+ .limit(limit);
50
+
51
+ return rows.map(parseMetadata);
52
+ }
53
+
54
+ /** All of the user's notifications, newest first. */
55
+ export async function getNotifications(userId: string, limit: number = 50) {
56
+ const rows = await db
57
+ .select()
58
+ .from(notifications)
59
+ .where(eq(notifications.userId, userId))
60
+ .orderBy(desc(notifications.createdAt))
61
+ .limit(limit);
62
+
63
+ return rows.map(parseMetadata);
64
+ }
65
+
66
+ export async function getNotificationCount(userId: string): Promise<number> {
67
+ const [result] = await db
68
+ .select({ count: count() })
69
+ .from(notifications)
70
+ .where(
71
+ and(eq(notifications.userId, userId), eq(notifications.isRead, false))
72
+ );
73
+
74
+ return result?.count ?? 0;
75
+ }
76
+
77
+ /** Filters on userId as well, so users can only mark their own. */
78
+ export async function markAsRead(notificationId: string, userId: string) {
79
+ await db
80
+ .update(notifications)
81
+ .set({ isRead: true })
82
+ .where(
83
+ and(
84
+ eq(notifications.id, notificationId),
85
+ eq(notifications.userId, userId)
86
+ )
87
+ );
88
+ }
89
+
90
+ export async function markAllAsRead(userId: string) {
91
+ await db
92
+ .update(notifications)
93
+ .set({ isRead: true })
94
+ .where(
95
+ and(eq(notifications.userId, userId), eq(notifications.isRead, false))
96
+ );
97
+ }
98
+
99
+ function parseMetadata<T extends { metadata: string | null }>(
100
+ notification: T
101
+ ): T & { metadata: Record<string, unknown> | null } {
102
+ return {
103
+ ...notification,
104
+ metadata: notification.metadata
105
+ ? (JSON.parse(notification.metadata) as Record<string, unknown>)
106
+ : null,
107
+ };
108
+ }