ekwo-os 0.4.1

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 (361) hide show
  1. package/README.md +873 -0
  2. package/dist/args.d.ts +57 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +134 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/assets/expected-objects.json +9432 -0
  7. package/dist/assets/migrations/20260911120000_core_companies.sql +260 -0
  8. package/dist/assets/migrations/20260911120100_accounts_journals.sql +211 -0
  9. package/dist/assets/migrations/20260911120200_contacts.sql +90 -0
  10. package/dist/assets/migrations/20260911120300_taxes.sql +146 -0
  11. package/dist/assets/migrations/20260911120400_entries.sql +338 -0
  12. package/dist/assets/migrations/20260911120500_documents.sql +279 -0
  13. package/dist/assets/migrations/20260911120600_payments_reconciliation.sql +251 -0
  14. package/dist/assets/migrations/20260911120700_bank.sql +178 -0
  15. package/dist/assets/migrations/20260911120800_analytics_attachments.sql +128 -0
  16. package/dist/assets/migrations/20260911120900_post_document.sql +279 -0
  17. package/dist/assets/migrations/20260911121000_reporting.sql +235 -0
  18. package/dist/assets/migrations/20260911121100_country_templates.sql +226 -0
  19. package/dist/assets/migrations/20260911121200_fec.sql +71 -0
  20. package/dist/assets/migrations/20260911130000_instance.sql +77 -0
  21. package/dist/assets/migrations/20260911130100_instance_members.sql +224 -0
  22. package/dist/assets/migrations/20260911140000_instance_admins.sql +112 -0
  23. package/dist/assets/migrations/20260911160000_tax_posting_templates_unique.sql +17 -0
  24. package/dist/assets/migrations/20260911170000_document_amount_paid.sql +82 -0
  25. package/dist/assets/migrations/20260911173000_post_payment.sql +122 -0
  26. package/dist/assets/migrations/20260911173100_sequence_counters_under_rls.sql +84 -0
  27. package/dist/assets/migrations/20260911183000_country_journal_defaults.sql +143 -0
  28. package/dist/assets/migrations/20260911193853_line_account_defaults.sql +274 -0
  29. package/dist/assets/migrations/20260911195054_products.sql +221 -0
  30. package/dist/assets/migrations/20260911210131_anon_surface.sql +85 -0
  31. package/dist/assets/migrations/20260912074712_country_packs.sql +318 -0
  32. package/dist/assets/migrations/20260912080311_report_code_and_region.sql +198 -0
  33. package/dist/assets/migrations/20260912081014_pack_certification_maintained.sql +31 -0
  34. package/dist/assets/migrations/20260912081015_pack_certification_backfill.sql +16 -0
  35. package/dist/assets/migrations/20260912090407_tax_report_boxes.sql +329 -0
  36. package/dist/assets/migrations/20260912091917_tax_on_base_value.sql +25 -0
  37. package/dist/assets/migrations/20260912091918_tax_engine_columns.sql +776 -0
  38. package/dist/assets/migrations/20260912094412_opening_and_closing.sql +759 -0
  39. package/dist/assets/migrations/20260912095825_charts_of_accounts.sql +326 -0
  40. package/dist/assets/migrations/20260912100412_financial_statements.sql +632 -0
  41. package/dist/assets/migrations/20260912104719_one_formula_evaluator.sql +209 -0
  42. package/dist/assets/migrations/20260912105720_entry_kind_appropriation.sql +26 -0
  43. package/dist/assets/migrations/20260912105721_appropriation_entry_kind.sql +444 -0
  44. package/dist/assets/migrations/20260912111751_document_rules.sql +274 -0
  45. package/dist/assets/migrations/20260912112132_cash_basis_vat_and_fx.sql +1189 -0
  46. package/dist/assets/migrations/20260913074512_modules.sql +493 -0
  47. package/dist/assets/migrations/20260913075903_asset_disposal_roles.sql +47 -0
  48. package/dist/assets/migrations/20260913083216_capabilities.sql +637 -0
  49. package/dist/assets/migrations/20260913083901_company_invitations.sql +262 -0
  50. package/dist/assets/migrations/20260913084402_user_preferences.sql +376 -0
  51. package/dist/assets/migrations/20260913084847_company_profile.sql +261 -0
  52. package/dist/assets/migrations/20260913085436_numbering_reads_the_pack.sql +235 -0
  53. package/dist/assets/migrations/20260913085932_api_keys.sql +303 -0
  54. package/dist/assets/migrations/20260913090216_fiscal_year_bounds.sql +159 -0
  55. package/dist/assets/migrations/20260913092527_entries_import.sql +219 -0
  56. package/dist/assets/migrations/20260913101536_null_safe_guards.sql +188 -0
  57. package/dist/assets/migrations/20260913102115_installer_is_named.sql +462 -0
  58. package/dist/assets/migrations/20260913102758_no_currency_default.sql +138 -0
  59. package/dist/assets/migrations/20260913103355_cash_basis_needs_a_box.sql +453 -0
  60. package/dist/assets/migrations/20260913104014_aged_balance_named_group.sql +79 -0
  61. package/dist/assets/migrations/20260913104232_foreign_key_indexes.sql +144 -0
  62. package/dist/assets/migrations/20260913105120_declared_no_reader.sql +41 -0
  63. package/dist/assets/migrations/20260913111407_pack_languages.sql +223 -0
  64. package/dist/assets/migrations/20260913112233_schema_comments_for_a_reader.sql +27 -0
  65. package/dist/assets/migrations/20260913114535_fec_opening_balances.sql +307 -0
  66. package/dist/assets/migrations/20260914103412_audit_log.sql +509 -0
  67. package/dist/assets/migrations/20260914111907_pack_upgrade.sql +428 -0
  68. package/dist/assets/migrations/20260914120500_rounding_reads_the_currency.sql +206 -0
  69. package/dist/assets/migrations/20260914121200_amounts_round_at_the_currency.sql +2403 -0
  70. package/dist/assets/migrations/20260914134325_schema_version_0_2_0.sql +36 -0
  71. package/dist/assets/migrations/20260914143915_accounts_in_use.sql +428 -0
  72. package/dist/assets/migrations/20260914144731_account_code_frozen.sql +99 -0
  73. package/dist/assets/migrations/20260914151207_schema_grants_its_own_rights.sql +284 -0
  74. package/dist/assets/migrations/20260914152840_pack_upgrade_records_its_own_line.sql +237 -0
  75. package/dist/assets/migrations/20260914163943_declaration_periodicity.sql +349 -0
  76. package/dist/assets/migrations/20260915094000_foreign_services_received.sql +37 -0
  77. package/dist/assets/migrations/20260915094500_mentions_read_a_foreign_service.sql +72 -0
  78. package/dist/assets/migrations/20260915153000_document_shares.sql +568 -0
  79. package/dist/assets/migrations/20260915160000_ec_sales_list.sql +181 -0
  80. package/dist/assets/migrations/20260915161842_pack_sources.sql +56 -0
  81. package/dist/assets/migrations/20260915170500_schema_version_0_3_0.sql +45 -0
  82. package/dist/assets/migrations/20260915174500_a_vat_category_is_a_code.sql +177 -0
  83. package/dist/assets/migrations/20260915180000_document_rule_references.sql +85 -0
  84. package/dist/assets/migrations/20260915181000_territories.sql +342 -0
  85. package/dist/assets/migrations/20260915181500_ec_sales_list_reads_the_territories.sql +222 -0
  86. package/dist/assets/migrations/20260915182000_intracom_triangular.sql +53 -0
  87. package/dist/assets/migrations/20260915182500_a_triangular_supply_says_reverse_charge.sql +99 -0
  88. package/dist/assets/migrations/20260915191200_a_document_knows_its_language.sql +633 -0
  89. package/dist/assets/migrations/20260915195000_a_price_that_holds_its_tax.sql +649 -0
  90. package/dist/assets/migrations/20260915200000_a_posting_names_its_boxes.sql +1004 -0
  91. package/dist/assets/migrations/20260916094500_a_company_files_more_than_one_declaration.sql +795 -0
  92. package/dist/assets/migrations/20260916103000_a_ledger_line_names_its_posting.sql +902 -0
  93. package/dist/assets/migrations/20260916123000_a_vocabulary_for_a_tax_that_is_not_a_vat.sql +144 -0
  94. package/dist/assets/migrations/20260916124000_a_box_can_be_a_rate_of_a_box.sql +525 -0
  95. package/dist/assets/migrations/20260916125000_a_tax_follows_the_territory.sql +1318 -0
  96. package/dist/assets/migrations/20260916126000_a_tax_point_and_its_exception.sql +1211 -0
  97. package/dist/assets/migrations/20260917090000_a_counterparty_that_learns.sql +561 -0
  98. package/dist/assets/migrations/20260917120000_what_the_money_pays.sql +545 -0
  99. package/dist/assets/migrations/20260917150000_a_filing_is_a_row.sql +488 -0
  100. package/dist/assets/migrations/20260917170000_when_it_is_due.sql +194 -0
  101. package/dist/assets/migrations/20260917180000_a_box_is_a_number_and_a_kind.sql +213 -0
  102. package/dist/assets/migrations/20260917190000_what_a_declaration_owes.sql +486 -0
  103. package/dist/assets/migrations/20260917200000_what_moved_after_it_went.sql +227 -0
  104. package/dist/assets/migrations/20260918090000_a_form_names_its_file.sql +27 -0
  105. package/dist/assets/migrations/20260918110000_a_deposit_and_what_came_back.sql +286 -0
  106. package/dist/assets/migrations/20260918113741_a_fourth_preset_is_named.sql +9 -0
  107. package/dist/assets/migrations/20260918113807_a_client_reads_and_hands_over.sql +96 -0
  108. package/dist/assets/migrations/20260918114322_two_definer_functions_check_their_caller.sql +75 -0
  109. package/dist/assets/migrations/20260918130000_every_company_somebody_keeps.sql +177 -0
  110. package/dist/assets/migrations/20260918140000_a_session_nobody_prepared.sql +40 -0
  111. package/dist/assets/migrations/20260918141107_a_line_keeps_the_tax_it_was_posted_with.sql +256 -0
  112. package/dist/assets/migrations/20260918141342_a_company_has_an_electronic_address.sql +66 -0
  113. package/dist/assets/migrations/20260918141605_an_invoice_reads_whole_from_the_views.sql +313 -0
  114. package/dist/assets/migrations/20260918141627_a_policy_asks_once.sql +132 -0
  115. package/dist/assets/migrations/20260918143352_a_suggestion_reads_the_words_once.sql +204 -0
  116. package/dist/assets/migrations/20260918143417_a_reference_of_the_caller_and_a_rehearsal.sql +115 -0
  117. package/dist/assets/migrations/20260918150712_a_company_leaves_with_its_books.sql +1119 -0
  118. package/dist/assets/migrations/20260918150931_a_statement_is_imported_once.sql +654 -0
  119. package/dist/assets/migrations/20260918161204_a_posted_document_does_not_move.sql +437 -0
  120. package/dist/assets/migrations/20260918161538_a_posted_entry_does_not_move.sql +252 -0
  121. package/dist/assets/migrations/20260918171946_an_entry_is_posted_by_post_entry.sql +289 -0
  122. package/dist/assets/migrations/20260918174312_schema_version_0_4_0.sql +53 -0
  123. package/dist/assets/modules/assets/module.json +13 -0
  124. package/dist/assets/modules/assets/supabase/migrations/20260913081447_assets.sql +1190 -0
  125. package/dist/assets/modules/assets/supabase/migrations/20260913104234_foreign_key_indexes.sql +47 -0
  126. package/dist/assets/modules/assets/supabase/migrations/20260913104500_assets_capabilities.sql +145 -0
  127. package/dist/assets/modules/assets/supabase/migrations/20260914122100_assets_round_at_the_currency.sql +564 -0
  128. package/dist/assets/modules/assets/supabase/migrations/20260914145012_assets_accounts_in_use.sql +37 -0
  129. package/dist/assets/modules/assets/supabase/migrations/20260914151530_assets_grants_its_own_rights.sql +63 -0
  130. package/dist/assets/modules/assets/supabase/migrations/20260918113912_assets_client_reads.sql +20 -0
  131. package/dist/assets/modules/assets/supabase/migrations/20260918150801_assets_leave_with_the_company.sql +41 -0
  132. package/dist/assets/modules/budgets/module.json +12 -0
  133. package/dist/assets/modules/budgets/supabase/migrations/20260913083012_budgets.sql +210 -0
  134. package/dist/assets/modules/budgets/supabase/migrations/20260913104233_foreign_key_indexes.sql +37 -0
  135. package/dist/assets/modules/budgets/supabase/migrations/20260913104501_budgets_capabilities.sql +72 -0
  136. package/dist/assets/modules/budgets/supabase/migrations/20260914122400_budgets_round_at_the_currency.sql +68 -0
  137. package/dist/assets/modules/budgets/supabase/migrations/20260914145048_budgets_accounts_in_use.sql +30 -0
  138. package/dist/assets/modules/budgets/supabase/migrations/20260914151812_budgets_grants_its_own_rights.sql +31 -0
  139. package/dist/assets/modules/budgets/supabase/migrations/20260918113954_budgets_client_reads.sql +20 -0
  140. package/dist/assets/modules/budgets/supabase/migrations/20260918150823_budgets_leave_with_the_company.sql +37 -0
  141. package/dist/assets/modules/schema/module.1.json +69 -0
  142. package/dist/assets/seed/00_currencies.sql +15 -0
  143. package/dist/assets/seed/00_territories.sql +280 -0
  144. package/dist/assets/seed/05_framework_generic.sql +110 -0
  145. package/dist/assets/seed/10_pack_be.sql +1284 -0
  146. package/dist/assets/seed/11_pack_fr.sql +1112 -0
  147. package/dist/assets/seed/12_pack_lu.sql +2484 -0
  148. package/dist/assets/seed/13_pack_ee.sql +733 -0
  149. package/dist/assets/seed/14_pack_gb.sql +796 -0
  150. package/dist/assets/seed/15_pack_us.sql +751 -0
  151. package/dist/assets/seed/90_demo_company.sql +368 -0
  152. package/dist/assets/seed/modules/assets/10_pack_be.sql +58 -0
  153. package/dist/assets/seed/modules/assets/11_pack_fr.sql +57 -0
  154. package/dist/assets/seed/modules/assets/14_pack_gb.sql +58 -0
  155. package/dist/assets/seed/modules/assets/15_pack_us.sql +60 -0
  156. package/dist/auth.d.ts +59 -0
  157. package/dist/auth.d.ts.map +1 -0
  158. package/dist/auth.js +134 -0
  159. package/dist/auth.js.map +1 -0
  160. package/dist/bin.d.ts +6 -0
  161. package/dist/bin.d.ts.map +1 -0
  162. package/dist/bin.js +8 -0
  163. package/dist/bin.js.map +1 -0
  164. package/dist/books.d.ts +60 -0
  165. package/dist/books.d.ts.map +1 -0
  166. package/dist/books.js +168 -0
  167. package/dist/books.js.map +1 -0
  168. package/dist/bootstrap.d.ts +236 -0
  169. package/dist/bootstrap.d.ts.map +1 -0
  170. package/dist/bootstrap.js +459 -0
  171. package/dist/bootstrap.js.map +1 -0
  172. package/dist/bundle.d.ts +27 -0
  173. package/dist/bundle.d.ts.map +1 -0
  174. package/dist/bundle.js +47 -0
  175. package/dist/bundle.js.map +1 -0
  176. package/dist/checklist.d.ts +35 -0
  177. package/dist/checklist.d.ts.map +1 -0
  178. package/dist/checklist.js +64 -0
  179. package/dist/checklist.js.map +1 -0
  180. package/dist/cli.d.ts +14 -0
  181. package/dist/cli.d.ts.map +1 -0
  182. package/dist/cli.js +318 -0
  183. package/dist/cli.js.map +1 -0
  184. package/dist/commands/company.d.ts +59 -0
  185. package/dist/commands/company.d.ts.map +1 -0
  186. package/dist/commands/company.js +287 -0
  187. package/dist/commands/company.js.map +1 -0
  188. package/dist/commands/contact.d.ts +9 -0
  189. package/dist/commands/contact.d.ts.map +1 -0
  190. package/dist/commands/contact.js +82 -0
  191. package/dist/commands/contact.js.map +1 -0
  192. package/dist/commands/demo.d.ts +19 -0
  193. package/dist/commands/demo.d.ts.map +1 -0
  194. package/dist/commands/demo.js +72 -0
  195. package/dist/commands/demo.js.map +1 -0
  196. package/dist/commands/doctor.d.ts +12 -0
  197. package/dist/commands/doctor.d.ts.map +1 -0
  198. package/dist/commands/doctor.js +68 -0
  199. package/dist/commands/doctor.js.map +1 -0
  200. package/dist/commands/document.d.ts +17 -0
  201. package/dist/commands/document.d.ts.map +1 -0
  202. package/dist/commands/document.js +217 -0
  203. package/dist/commands/document.js.map +1 -0
  204. package/dist/commands/init.d.ts +31 -0
  205. package/dist/commands/init.d.ts.map +1 -0
  206. package/dist/commands/init.js +535 -0
  207. package/dist/commands/init.js.map +1 -0
  208. package/dist/commands/login.d.ts +24 -0
  209. package/dist/commands/login.d.ts.map +1 -0
  210. package/dist/commands/login.js +128 -0
  211. package/dist/commands/login.js.map +1 -0
  212. package/dist/commands/migrate.d.ts +30 -0
  213. package/dist/commands/migrate.d.ts.map +1 -0
  214. package/dist/commands/migrate.js +115 -0
  215. package/dist/commands/migrate.js.map +1 -0
  216. package/dist/commands/module.d.ts +32 -0
  217. package/dist/commands/module.d.ts.map +1 -0
  218. package/dist/commands/module.js +285 -0
  219. package/dist/commands/module.js.map +1 -0
  220. package/dist/commands/pack.d.ts +22 -0
  221. package/dist/commands/pack.d.ts.map +1 -0
  222. package/dist/commands/pack.js +481 -0
  223. package/dist/commands/pack.js.map +1 -0
  224. package/dist/commands/payment.d.ts +15 -0
  225. package/dist/commands/payment.d.ts.map +1 -0
  226. package/dist/commands/payment.js +97 -0
  227. package/dist/commands/payment.js.map +1 -0
  228. package/dist/commands/register.d.ts +20 -0
  229. package/dist/commands/register.d.ts.map +1 -0
  230. package/dist/commands/register.js +134 -0
  231. package/dist/commands/register.js.map +1 -0
  232. package/dist/commands/status.d.ts +8 -0
  233. package/dist/commands/status.d.ts.map +1 -0
  234. package/dist/commands/status.js +134 -0
  235. package/dist/commands/status.js.map +1 -0
  236. package/dist/commands/whoami.d.ts +21 -0
  237. package/dist/commands/whoami.d.ts.map +1 -0
  238. package/dist/commands/whoami.js +133 -0
  239. package/dist/commands/whoami.js.map +1 -0
  240. package/dist/company.d.ts +16 -0
  241. package/dist/company.d.ts.map +1 -0
  242. package/dist/company.js +26 -0
  243. package/dist/company.js.map +1 -0
  244. package/dist/config.d.ts +24 -0
  245. package/dist/config.d.ts.map +1 -0
  246. package/dist/config.js +50 -0
  247. package/dist/config.js.map +1 -0
  248. package/dist/connection.d.ts +79 -0
  249. package/dist/connection.d.ts.map +1 -0
  250. package/dist/connection.js +116 -0
  251. package/dist/connection.js.map +1 -0
  252. package/dist/context.d.ts +47 -0
  253. package/dist/context.d.ts.map +1 -0
  254. package/dist/context.js +93 -0
  255. package/dist/context.js.map +1 -0
  256. package/dist/doctor.d.ts +45 -0
  257. package/dist/doctor.d.ts.map +1 -0
  258. package/dist/doctor.js +448 -0
  259. package/dist/doctor.js.map +1 -0
  260. package/dist/grants.d.ts +88 -0
  261. package/dist/grants.d.ts.map +1 -0
  262. package/dist/grants.js +174 -0
  263. package/dist/grants.js.map +1 -0
  264. package/dist/identity.d.ts +54 -0
  265. package/dist/identity.d.ts.map +1 -0
  266. package/dist/identity.js +132 -0
  267. package/dist/identity.js.map +1 -0
  268. package/dist/index.d.ts +44 -0
  269. package/dist/index.d.ts.map +1 -0
  270. package/dist/index.js +43 -0
  271. package/dist/index.js.map +1 -0
  272. package/dist/inventory.d.ts +155 -0
  273. package/dist/inventory.d.ts.map +1 -0
  274. package/dist/inventory.js +303 -0
  275. package/dist/inventory.js.map +1 -0
  276. package/dist/migrations.d.ts +76 -0
  277. package/dist/migrations.d.ts.map +1 -0
  278. package/dist/migrations.js +226 -0
  279. package/dist/migrations.js.map +1 -0
  280. package/dist/module/read.d.ts +91 -0
  281. package/dist/module/read.d.ts.map +1 -0
  282. package/dist/module/read.js +172 -0
  283. package/dist/module/read.js.map +1 -0
  284. package/dist/output.d.ts +110 -0
  285. package/dist/output.d.ts.map +1 -0
  286. package/dist/output.js +173 -0
  287. package/dist/output.js.map +1 -0
  288. package/dist/pack/certification.d.ts +27 -0
  289. package/dist/pack/certification.d.ts.map +1 -0
  290. package/dist/pack/certification.js +38 -0
  291. package/dist/pack/certification.js.map +1 -0
  292. package/dist/pack/compile.d.ts +75 -0
  293. package/dist/pack/compile.d.ts.map +1 -0
  294. package/dist/pack/compile.js +801 -0
  295. package/dist/pack/compile.js.map +1 -0
  296. package/dist/pack/filing.d.ts +48 -0
  297. package/dist/pack/filing.d.ts.map +1 -0
  298. package/dist/pack/filing.js +56 -0
  299. package/dist/pack/filing.js.map +1 -0
  300. package/dist/pack/read.d.ts +766 -0
  301. package/dist/pack/read.d.ts.map +1 -0
  302. package/dist/pack/read.js +2336 -0
  303. package/dist/pack/read.js.map +1 -0
  304. package/dist/pack/schema.d.ts +26 -0
  305. package/dist/pack/schema.d.ts.map +1 -0
  306. package/dist/pack/schema.js +181 -0
  307. package/dist/pack/schema.js.map +1 -0
  308. package/dist/pack/territories.d.ts +98 -0
  309. package/dist/pack/territories.d.ts.map +1 -0
  310. package/dist/pack/territories.js +0 -0
  311. package/dist/pack/territories.js.map +1 -0
  312. package/dist/pack/upgrade.d.ts +86 -0
  313. package/dist/pack/upgrade.d.ts.map +1 -0
  314. package/dist/pack/upgrade.js +76 -0
  315. package/dist/pack/upgrade.js.map +1 -0
  316. package/dist/pack/vat-codes.d.ts +273 -0
  317. package/dist/pack/vat-codes.d.ts.map +1 -0
  318. package/dist/pack/vat-codes.js +513 -0
  319. package/dist/pack/vat-codes.js.map +1 -0
  320. package/dist/profiles.d.ts +67 -0
  321. package/dist/profiles.d.ts.map +1 -0
  322. package/dist/profiles.js +187 -0
  323. package/dist/profiles.js.map +1 -0
  324. package/dist/prompt.d.ts +40 -0
  325. package/dist/prompt.d.ts.map +1 -0
  326. package/dist/prompt.js +120 -0
  327. package/dist/prompt.js.map +1 -0
  328. package/dist/registry.d.ts +84 -0
  329. package/dist/registry.d.ts.map +1 -0
  330. package/dist/registry.js +104 -0
  331. package/dist/registry.js.map +1 -0
  332. package/dist/rest.d.ts +81 -0
  333. package/dist/rest.d.ts.map +1 -0
  334. package/dist/rest.js +245 -0
  335. package/dist/rest.js.map +1 -0
  336. package/dist/schema.d.ts +15 -0
  337. package/dist/schema.d.ts.map +1 -0
  338. package/dist/schema.js +15 -0
  339. package/dist/schema.js.map +1 -0
  340. package/dist/seeds.d.ts +27 -0
  341. package/dist/seeds.d.ts.map +1 -0
  342. package/dist/seeds.js +47 -0
  343. package/dist/seeds.js.map +1 -0
  344. package/dist/session.d.ts +39 -0
  345. package/dist/session.d.ts.map +1 -0
  346. package/dist/session.js +92 -0
  347. package/dist/session.js.map +1 -0
  348. package/dist/sql.d.ts +48 -0
  349. package/dist/sql.d.ts.map +1 -0
  350. package/dist/sql.js +112 -0
  351. package/dist/sql.js.map +1 -0
  352. package/dist/status.d.ts +94 -0
  353. package/dist/status.d.ts.map +1 -0
  354. package/dist/status.js +120 -0
  355. package/dist/status.js.map +1 -0
  356. package/dist/ui.d.ts +55 -0
  357. package/dist/ui.d.ts.map +1 -0
  358. package/dist/ui.js +114 -0
  359. package/dist/ui.js.map +1 -0
  360. package/package.json +29 -0
  361. package/schema/output.1.json +613 -0
@@ -0,0 +1,568 @@
1
+ -- Ekwo OS — a document, opened by whoever holds its link.
2
+ --
3
+ -- A company sends an invoice and the customer wants to look at it: what it
4
+ -- says, what is still owed on it, which sentences the law put at its foot. The
5
+ -- three answers already exist in this schema — `document_header`,
6
+ -- `document_line_items`, `document_tax_summary`, `document_legal_mentions`,
7
+ -- `documents.amount_residual` — and every one of them is behind row level
8
+ -- security, which is right: the customer is not a member of the company and
9
+ -- never will be. So the usual answers are both bad. A login for a customer who
10
+ -- will read one invoice is an account nobody maintains; an attachment is a
11
+ -- copy that stops being true the day the invoice is paid.
12
+ --
13
+ -- A **share** is the third answer: a row that says *this document may be read
14
+ -- by whoever presents this secret*, and one function that takes the secret and
15
+ -- returns the document as it was sent, with what is still owed on it today.
16
+ --
17
+ -- Six decisions, each of them a thing that could have been done otherwise.
18
+ --
19
+ -- **The token is the whole secret, and only its hash is stored.** 32 bytes,
20
+ -- rendered base64url, 43 characters. `document_shares.token_hash` is a sha256
21
+ -- of it and the clear token is returned by `share_document()` and by nothing
22
+ -- else, ever — the same arrangement `company_invitations` and `api_keys` are
23
+ -- already built on, and for the same reason: a database backup, a replica or a
24
+ -- support engineer reading a table hands nobody a live link.
25
+ --
26
+ -- **Where the 32 bytes come from is not `gen_random_bytes`.** That function is
27
+ -- `pgcrypto`, and `pgcrypto` is not available under PGlite, which is what this
28
+ -- repository's whole test suite runs on — an invariant that cannot be tested
29
+ -- is an invariant nobody is keeping. `gen_random_uuid()` is core Postgres and
30
+ -- is the source the primary keys of every table here already come from; two of
31
+ -- them are 32 bytes, of which 244 bits are random (a v4 UUID fixes six bits for
32
+ -- its version and its variant). 244 bits is far past the point where guessing
33
+ -- is the attack anybody would choose, and `sha256()` is core too, since
34
+ -- PostgreSQL 11. No extension is added by this migration and none is needed.
35
+ --
36
+ -- **`anon` gets one function and not one table.** The invariant of
37
+ -- `20260911210131` and `20260914151207` — the anonymous role holds no
38
+ -- privilege on any table or view of this schema, and reaches only functions
39
+ -- that answer about the caller — is not bent here. `shared_document()` is
40
+ -- `security definer`: the reader has no rights of their own, the function has
41
+ -- them, and what comes out is one document rendered into one jsonb. There is
42
+ -- no view to select from, no filter to widen and no second row to reach.
43
+ --
44
+ -- **No IP address and no user agent.** Who opened a link and from where is a
45
+ -- log, and a log of the people a company invoices is personal data with a
46
+ -- retention policy, a lawful basis and a subject-access request behind it. The
47
+ -- core records `view_count` and `last_viewed_at`, which is what the sender
48
+ -- actually asks ("did they open it?"), and the application in front of it
49
+ -- records whatever it is prepared to answer for.
50
+ --
51
+ -- **A share is not edited.** There is no `update` policy and no
52
+ -- `change_share()`. An expiry that moves is a link whose lifetime is a moving
53
+ -- target, and a token that outlives the decision to withdraw it is the one
54
+ -- failure a sharing feature must not have. Revoke it and make another.
55
+ --
56
+ -- **Sales only.** A purchase invoice is a document somebody else wrote about
57
+ -- their own business: their prices, their bank details, their legal mentions.
58
+ -- Publishing it under a link this company controls would be publishing a third
59
+ -- party's data, and there is no request behind it — nobody shares a supplier's
60
+ -- invoice with the supplier. `share_document()` refuses it by name.
61
+
62
+ -- ---------------------------------------------------------------------------
63
+ -- 1. The instance learns its own address
64
+ --
65
+ -- A link has to be absolute, and nothing in this schema knew where the
66
+ -- installation answers. The column is nullable, carries no default, and no URL
67
+ -- of Ekwo's is written anywhere: a self-hosted installation is reachable at
68
+ -- the address its operator chose, and a core that guessed would hand out links
69
+ -- to somebody else's host. Null is a supported state — `share_document()`
70
+ -- returns the token with a null `url`, and the caller builds the link itself.
71
+ -- ---------------------------------------------------------------------------
72
+
73
+ alter table instance
74
+ add column if not exists public_base_url text;
75
+
76
+ comment on column instance.public_base_url is
77
+ 'Where this installation answers on the public internet, as an origin with no trailing slash — the base a shared document link is built on. Null where the operator has not said, and then a share returns its token with no URL.';
78
+
79
+ alter table instance
80
+ add constraint instance_public_base_url_absolute
81
+ check (public_base_url is null or public_base_url ~ '^https?://[^[:space:]]+$');
82
+
83
+ -- ---------------------------------------------------------------------------
84
+ -- 2. What a share is about
85
+ --
86
+ -- One value today. It exists because the second one is already foreseeable —
87
+ -- a statement of account, a financial statement, an ageing balance sent to a
88
+ -- customer are the same act with a different subject — and because the shape
89
+ -- that survives it is decided now or paid for later. `document_id` is nullable
90
+ -- and tied to the kind by a check, so the kind that arrives next adds its own
91
+ -- column and its own branch of the same check, and every row written before it
92
+ -- keeps meaning exactly what it meant.
93
+ -- ---------------------------------------------------------------------------
94
+
95
+ create type share_subject_kind as enum ('document');
96
+
97
+ comment on type share_subject_kind is
98
+ 'What a share gives access to. One value today; a statement or a report is the same act with another subject, and the column is here so that arriving costs a branch rather than a rewrite.';
99
+
100
+ create table document_shares (
101
+ id uuid primary key default gen_random_uuid(),
102
+ company_id uuid not null references companies(id) on delete cascade,
103
+ subject_kind share_subject_kind not null default 'document',
104
+ document_id uuid references documents(id) on delete cascade,
105
+ -- sha256 of the token, hex. Unique, because presenting a token is a lookup
106
+ -- on this column and two rows answering it would be two links nobody can
107
+ -- tell apart.
108
+ token_hash text not null unique,
109
+ expires_at timestamptz,
110
+ revoked_at timestamptz,
111
+ created_by uuid,
112
+ created_at timestamptz not null default now(),
113
+ view_count integer not null default 0,
114
+ last_viewed_at timestamptz,
115
+ constraint document_shares_subject_matches_kind
116
+ check ((subject_kind = 'document') = (document_id is not null)),
117
+ constraint document_shares_expiry check (expires_at is null or expires_at > created_at),
118
+ constraint document_shares_view_count_positive check (view_count >= 0),
119
+ constraint document_shares_token_hash_shape check (token_hash ~ '^[0-9a-f]{64}$'),
120
+ foreign key (document_id, company_id) references documents(id, company_id) on delete cascade
121
+ );
122
+
123
+ comment on table document_shares is
124
+ 'Public links onto a document. The token is handed over once and kept only as a sha256; the link is withdrawn by revoking it, never by editing it.';
125
+ comment on column document_shares.token_hash is
126
+ 'sha256 of the token, hex. The token itself is returned by share_document() and stored nowhere.';
127
+ comment on column document_shares.expires_at is
128
+ 'When the link stops answering. Null means it answers until it is revoked, which is a deliberate choice and not an oversight: an invoice is looked at years later.';
129
+ comment on column document_shares.revoked_at is
130
+ 'When the link was withdrawn. A withdrawn link answers exactly like one that never existed.';
131
+ comment on column document_shares.created_by is
132
+ 'auth.users.id of whoever created it. No foreign key, for the same reason company_members has none.';
133
+ comment on column document_shares.view_count is
134
+ 'How many times the document was read through this link. No address and no user agent: who opened it and from where is a log the application keeps, with the retention policy that goes with it.';
135
+
136
+ -- The second one leads with the two columns of the composite foreign key, in
137
+ -- its order, which is the rule `20260913104232` set and a test enforces: a key
138
+ -- without an index is a sequential scan on every delete of the parent row.
139
+ create index document_shares_company_idx on document_shares (company_id, created_at desc);
140
+ create index document_shares_document_idx on document_shares (document_id, company_id);
141
+
142
+ -- ---------------------------------------------------------------------------
143
+ -- 3. Sharing is its own capability
144
+ --
145
+ -- `documents.write` would have been close enough to work and wrong to read: a
146
+ -- bookkeeper who drafts invoices is not necessarily the person a company wants
147
+ -- publishing them on the open internet, and the two acts are refused and
148
+ -- granted separately the first time somebody asks. It sits on the two presets
149
+ -- that issue documents, and on neither the viewer nor anything below.
150
+ -- ---------------------------------------------------------------------------
151
+
152
+ insert into capabilities (code, area, description) values
153
+ ('documents.share', 'documents',
154
+ 'Publish a sales document behind a link anyone holding it can open, and withdraw such a link.')
155
+ on conflict (code) do nothing;
156
+
157
+ insert into role_capabilities (role, capability)
158
+ select r.role, 'documents.share'
159
+ from (values ('owner'::member_role), ('accountant'::member_role)) as r(role)
160
+ on conflict do nothing;
161
+
162
+ -- ---------------------------------------------------------------------------
163
+ -- 4. Which documents may be shared, and the refusal each one gets
164
+ --
165
+ -- Four rules, and a named refusal for each, because "not allowed" on an
166
+ -- invoice somebody is trying to send is the least useful sentence a database
167
+ -- can produce. Written once here and read by both `share_document()` — which
168
+ -- refuses — and `shared_document()` — which stops answering the day the
169
+ -- document leaves the set, so cancelling an invoice closes the links onto it
170
+ -- without anybody having to remember to revoke them.
171
+ -- ---------------------------------------------------------------------------
172
+
173
+ create or replace function document_share_refusal(p_document documents)
174
+ returns text
175
+ language sql
176
+ immutable
177
+ as $$
178
+ select case
179
+ when p_document.doc_type::text not like 'sale%' then
180
+ 'share_not_a_sale: a ' || p_document.doc_type::text ||
181
+ ' is a third party''s own document — their prices, their bank details, their mentions — and this installation does not publish it'
182
+ when p_document.state = 'cancelled' then
183
+ 'share_cancelled_document: that document was cancelled, and a cancelled document is not sent to anybody'
184
+ when p_document.doc_type in ('sale_invoice', 'sale_credit_note')
185
+ and p_document.state = 'draft' then
186
+ 'share_draft_document: that document is still a draft. Post it — a draft has no number, carries no entry, and is not what was sent'
187
+ when p_document.number is null then
188
+ 'share_unnumbered_document: that document carries no number, so there is nothing to show a customer that they can quote back'
189
+ else null
190
+ end;
191
+ $$;
192
+
193
+ comment on function document_share_refusal(documents) is
194
+ 'Why this document may not be shared, or null when it may. Sales only, never cancelled, never an unposted invoice, always numbered.';
195
+
196
+ -- ---------------------------------------------------------------------------
197
+ -- 5. Creating a link
198
+ --
199
+ -- `security definer`, for three reasons at once: the token has to be hashed
200
+ -- where nobody sees it, `audit_record()` is reachable by no client, and a
201
+ -- caller who may share a document is not necessarily a caller who may insert
202
+ -- into a table nothing else writes.
203
+ --
204
+ -- The token comes back in this answer and in no other. The URL is the
205
+ -- installation's base plus `/shared/<token>` — one path, written down in
206
+ -- `docs/sharing.md`, so that an operator putting a reverse proxy in front of
207
+ -- their installation knows which route has to reach the application.
208
+ -- ---------------------------------------------------------------------------
209
+
210
+ create or replace function share_document(
211
+ p_document_id uuid,
212
+ p_expires_at timestamptz default null
213
+ )
214
+ returns table (share_id uuid, token text, url text)
215
+ language plpgsql
216
+ security definer
217
+ set search_path = public, pg_temp
218
+ as $$
219
+ declare
220
+ v_doc documents%rowtype;
221
+ v_refusal text;
222
+ v_token text;
223
+ v_base text;
224
+ v_row document_shares%rowtype;
225
+ begin
226
+ select * into v_doc from documents where id = p_document_id;
227
+ if v_doc.id is null then
228
+ raise exception 'unknown_document: document % does not exist', p_document_id;
229
+ end if;
230
+
231
+ if not is_installer() and not has_capability(v_doc.company_id, 'documents.share') then
232
+ raise exception 'not_allowed: publishing a document of this company needs documents.share'
233
+ using errcode = '42501';
234
+ end if;
235
+
236
+ v_refusal := document_share_refusal(v_doc);
237
+ if v_refusal is not null then
238
+ raise exception '%', v_refusal;
239
+ end if;
240
+
241
+ if p_expires_at is not null and p_expires_at <= now() then
242
+ raise exception 'share_expires_in_the_past: % is not in the future, so this link would be born dead',
243
+ p_expires_at;
244
+ end if;
245
+
246
+ -- 32 bytes as base64url, 43 characters, no padding. `translate` maps the two
247
+ -- characters base64 and base64url disagree on and drops the `=`, whose
248
+ -- position is implied by the length.
249
+ v_token := translate(
250
+ encode(
251
+ decode(replace(gen_random_uuid()::text, '-', '') ||
252
+ replace(gen_random_uuid()::text, '-', ''), 'hex'),
253
+ 'base64'),
254
+ '+/=', '-_');
255
+
256
+ insert into document_shares (company_id, subject_kind, document_id, token_hash,
257
+ expires_at, created_by)
258
+ values (v_doc.company_id, 'document', v_doc.id,
259
+ encode(sha256(convert_to(v_token, 'UTF8')), 'hex'),
260
+ p_expires_at, auth.uid())
261
+ returning * into v_row;
262
+
263
+ perform audit_record(
264
+ v_doc.company_id, 'document_shares', v_row.id,
265
+ coalesce(v_doc.number, v_doc.id::text), 'insert', 'document_shared',
266
+ null,
267
+ jsonb_build_object('document_id', v_doc.id, 'doc_type', v_doc.doc_type,
268
+ 'expires_at', v_row.expires_at));
269
+
270
+ select i.public_base_url into v_base from instance i where i.id = 1;
271
+
272
+ return query select v_row.id,
273
+ v_token,
274
+ case when v_base is null then null
275
+ else rtrim(v_base, '/') || '/shared/' || v_token end;
276
+ end;
277
+ $$;
278
+
279
+ comment on function share_document(uuid, timestamptz) is
280
+ 'Publishes a sales document behind a link and returns the token once — only its hash is stored. `url` is the instance''s public base plus /shared/<token>, or null where the instance has not recorded one. A share is never edited: revoke it and make another.';
281
+
282
+ create or replace function revoke_share(p_share_id uuid)
283
+ returns document_shares
284
+ language plpgsql
285
+ security definer
286
+ set search_path = public, pg_temp
287
+ as $$
288
+ declare
289
+ v_row document_shares%rowtype;
290
+ begin
291
+ select * into v_row from document_shares where id = p_share_id;
292
+ if v_row.id is null then
293
+ raise exception 'unknown_share: no share with that id';
294
+ end if;
295
+
296
+ if not is_installer() and not has_capability(v_row.company_id, 'documents.share') then
297
+ raise exception 'not_allowed: withdrawing a link of this company needs documents.share'
298
+ using errcode = '42501';
299
+ end if;
300
+
301
+ update document_shares
302
+ set revoked_at = coalesce(revoked_at, now())
303
+ where id = p_share_id
304
+ returning * into v_row;
305
+
306
+ perform audit_record(
307
+ v_row.company_id, 'document_shares', v_row.id,
308
+ (select coalesce(d.number, d.id::text) from documents d where d.id = v_row.document_id),
309
+ 'update', 'document_share_revoked',
310
+ null, jsonb_build_object('revoked_at', v_row.revoked_at));
311
+
312
+ return v_row;
313
+ end;
314
+ $$;
315
+
316
+ comment on function revoke_share(uuid) is
317
+ 'Withdraws a link, now and for good. A withdrawn link answers exactly like one that never existed; there is no un-withdraw, because a secret that has been out of the building is issued again rather than brought back.';
318
+
319
+ -- ---------------------------------------------------------------------------
320
+ -- 6. Reading one
321
+ --
322
+ -- The public door. `anon` may execute it and it takes one argument: the token
323
+ -- is the whole of what the caller presents and the whole of what the function
324
+ -- consults, so there is nothing to enumerate and nothing to widen. Unknown,
325
+ -- revoked, expired, and a document that has since left the shareable set all
326
+ -- produce the *same* answer — SQL null — because an error message that
327
+ -- distinguished them would tell somebody feeding tokens at it which of their
328
+ -- guesses had been a real link.
329
+ --
330
+ -- What comes out is the document as it was sent, plus the one thing that
331
+ -- legitimately moves after sending: what is still owed. Every figure is read
332
+ -- from the views the renderer of this schema already uses, so a shared link
333
+ -- and a printed invoice cannot come to disagree.
334
+ --
335
+ -- What deliberately does not come out: no identifier of anything — not the
336
+ -- document, not the company, not the contact, not a tax, not an account —
337
+ -- no analytic axis, no cost, no internal contact note, no list of anything
338
+ -- the company owns. A link is one document, and nothing it carries can be
339
+ -- turned into a question about a second one.
340
+ --
341
+ -- Every amount is rendered as a decimal *string*. A JSON number is a double
342
+ -- by the time it reaches a browser, and an invoice total that arrives as
343
+ -- 1210.0000000000002 is the oldest bug in billing software. It is also what
344
+ -- the rest of this project already says on the wire: the MCP server tells its
345
+ -- clients that amounts are decimal strings, and `::text` on a numeric keeps
346
+ -- the scale the column was rounded at.
347
+ -- ---------------------------------------------------------------------------
348
+
349
+ create or replace function shared_document(p_token text)
350
+ returns jsonb
351
+ language plpgsql
352
+ security definer
353
+ set search_path = public, pg_temp
354
+ as $$
355
+ declare
356
+ v_share document_shares%rowtype;
357
+ v_doc documents%rowtype;
358
+ v_header document_header%rowtype;
359
+ v_language text;
360
+ v_last_payment date;
361
+ v_payload jsonb;
362
+ begin
363
+ select * into v_share
364
+ from document_shares
365
+ where token_hash = encode(sha256(convert_to(coalesce(p_token, ''), 'UTF8')), 'hex');
366
+
367
+ if v_share.id is null
368
+ or v_share.revoked_at is not null
369
+ or (v_share.expires_at is not null and v_share.expires_at <= now())
370
+ or v_share.subject_kind <> 'document' then
371
+ return null;
372
+ end if;
373
+
374
+ select * into v_doc from documents where id = v_share.document_id;
375
+ if v_doc.id is null or document_share_refusal(v_doc) is not null then
376
+ return null;
377
+ end if;
378
+
379
+ -- The link was used. Counted before the payload is built, so a reader who
380
+ -- gives up halfway is still a reader.
381
+ update document_shares
382
+ set view_count = view_count + 1,
383
+ last_viewed_at = now()
384
+ where id = v_share.id;
385
+
386
+ select * into v_header from document_header where document_id = v_doc.id;
387
+
388
+ -- The language the document speaks: the one the customer reads, else the one
389
+ -- the company keeps its books in. `companies.language` is not null — it is
390
+ -- filled from the pack when the company is created — so the chain always
391
+ -- ends somewhere, and it ends without a literal. A language written into
392
+ -- this function would be one country's answer printed on every other
393
+ -- country's invoice.
394
+ --
395
+ -- `preferred_languages()` is the published way of choosing a language and it
396
+ -- cannot be used here: it starts at the preferences of `auth.uid()`, which
397
+ -- is null for the one reader this function exists for. The gap is written
398
+ -- down in `docs/international.md`.
399
+ select coalesce(ct.language, c.language)
400
+ into v_language
401
+ from documents d
402
+ join companies c on c.id = d.company_id
403
+ join contacts ct on ct.id = d.contact_id
404
+ where d.id = v_doc.id;
405
+
406
+ -- When the last money against it arrived. The other side of every matching
407
+ -- on the document's own third-party lines, where that side is a payment.
408
+ select max(p.payment_date) into v_last_payment
409
+ from entry_lines dl
410
+ join reconciliations r
411
+ on r.debit_line_id = dl.id or r.credit_line_id = dl.id
412
+ join entry_lines ol
413
+ on ol.id = case when r.debit_line_id = dl.id then r.credit_line_id else r.debit_line_id end
414
+ join payments p on p.entry_id = ol.entry_id
415
+ where dl.entry_id = v_doc.entry_id;
416
+
417
+ v_payload := jsonb_build_object(
418
+ 'document', jsonb_build_object(
419
+ 'type', v_header.doc_type,
420
+ 'number', v_header.number,
421
+ 'document_date', v_header.document_date,
422
+ 'due_date', v_header.due_date,
423
+ 'delivery_date', v_header.delivery_date,
424
+ 'currency', v_header.currency_code,
425
+ 'language', v_language,
426
+ 'payment_terms', v_header.payment_terms,
427
+ 'payment_reference', v_header.payment_reference,
428
+ 'buyer_reference', v_header.buyer_reference,
429
+ 'order_reference', v_header.order_reference,
430
+ -- BT-22, the note the seller wrote on the invoice itself. An internal
431
+ -- remark about a customer is `contacts.notes` and is not in this object.
432
+ 'note', v_header.note
433
+ ),
434
+ 'seller', jsonb_build_object(
435
+ 'name', v_header.seller_name,
436
+ 'legal_name', v_header.seller_legal_name,
437
+ 'legal_form', v_header.seller_legal_form,
438
+ 'vat_number', v_header.seller_vat_number,
439
+ 'registration_number', v_header.seller_registration_number,
440
+ 'address_line1', v_header.seller_address_line1,
441
+ 'address_line2', v_header.seller_address_line2,
442
+ 'postal_code', v_header.seller_postal_code,
443
+ 'city', v_header.seller_city,
444
+ 'country', v_header.seller_country,
445
+ 'email', v_header.seller_email,
446
+ 'phone', v_header.seller_phone,
447
+ 'website', v_header.seller_website,
448
+ 'logo_url', v_header.seller_logo_url,
449
+ 'iban', v_header.payee_iban,
450
+ 'bic', v_header.payee_bic
451
+ ),
452
+ 'buyer', jsonb_build_object(
453
+ 'name', v_header.buyer_name,
454
+ 'vat_number', v_header.buyer_vat_number,
455
+ 'registration_number', v_header.buyer_registration_number,
456
+ 'address_line1', v_header.buyer_address_line1,
457
+ 'address_line2', v_header.buyer_address_line2,
458
+ 'postal_code', v_header.buyer_postal_code,
459
+ 'city', v_header.buyer_city,
460
+ 'country', v_header.buyer_country
461
+ ),
462
+ 'lines', coalesce((
463
+ select jsonb_agg(jsonb_build_object(
464
+ 'sequence', li.sequence,
465
+ 'type', li.line_type,
466
+ 'name', li.item_name,
467
+ 'description', li.item_description,
468
+ 'quantity', li.quantity::text,
469
+ 'unit_code', li.unit_code,
470
+ 'unit_price', li.unit_price::text,
471
+ 'discount_percent', li.discount_percent::text,
472
+ 'amount_untaxed', li.amount_untaxed::text,
473
+ 'tax_category', li.vat_category,
474
+ 'tax_rate', li.vat_rate::text,
475
+ 'tax_exemption_code', li.tax_exemption_code)
476
+ order by li.sequence)
477
+ from document_line_items li
478
+ where li.document_id = v_doc.id), '[]'::jsonb),
479
+ 'tax_summary', coalesce((
480
+ select jsonb_agg(jsonb_build_object(
481
+ 'name', ts.tax_name,
482
+ 'category', ts.vat_category,
483
+ 'rate', ts.tax_rate::text,
484
+ 'base_amount', ts.base_amount::text,
485
+ -- What the other party actually pays, which is what an invoice
486
+ -- prints: zero where the tax self-assesses, and what the four
487
+ -- totals below add up from.
488
+ 'tax_amount', ts.tax_charged::text)
489
+ order by ts.tax_rate, ts.tax_name)
490
+ from document_tax_summary ts
491
+ where ts.document_id = v_doc.id), '[]'::jsonb),
492
+ 'totals', jsonb_build_object(
493
+ 'amount_untaxed', v_header.amount_untaxed::text,
494
+ 'amount_tax', v_header.amount_tax::text,
495
+ 'amount_total', v_header.amount_total::text
496
+ ),
497
+ -- The one thing that legitimately moves after the document was sent, so a
498
+ -- link a customer keeps stays worth opening.
499
+ 'payment', jsonb_build_object(
500
+ 'state', v_header.payment_state,
501
+ 'amount_paid', v_header.amount_paid::text,
502
+ 'amount_residual', v_header.amount_residual::text,
503
+ 'last_payment_date', v_last_payment
504
+ ),
505
+ 'legal_mentions', coalesce((
506
+ select jsonb_agg(jsonb_build_object(
507
+ 'code', lm.code,
508
+ 'text', label_for(lm.text, lm.text_i18n, array[v_language]))
509
+ order by lm.sequence, lm.code)
510
+ from document_legal_mentions lm
511
+ where lm.document_id = v_doc.id), '[]'::jsonb)
512
+ );
513
+
514
+ return v_payload;
515
+ end;
516
+ $$;
517
+
518
+ comment on function shared_document(text) is
519
+ 'One document, read by whoever holds its link: the header, the lines, the tax breakdown, the totals, the legal mentions in the document''s own language, and what is still owed today. Returns null — the same null, in the same shape — for a token that is unknown, withdrawn, expired, or onto a document that may no longer be shared.';
520
+
521
+ -- ---------------------------------------------------------------------------
522
+ -- 7. Row level security
523
+ --
524
+ -- Reading the links of a company is reading its documents, so it is
525
+ -- `documents.read` — which every preset holds, viewer included, because seeing
526
+ -- that an invoice was published is part of reading it. Nothing writes the
527
+ -- table through the API: the two functions above are the only way in, and each
528
+ -- checks what a policy would have. That is the shape `api_keys` and
529
+ -- `company_invitations` already have, and it is why `list_shares` is a select
530
+ -- and not a fourth function — a list that row level security already produces
531
+ -- correctly does not need a function to produce it a second way.
532
+ --
533
+ -- There is no policy for `anon`, on this table or on any other. What an
534
+ -- anonymous reader reaches is one `security definer` function, and the table
535
+ -- under it is as closed to them as every other table of this schema.
536
+ -- ---------------------------------------------------------------------------
537
+
538
+ alter table document_shares enable row level security;
539
+
540
+ create policy document_shares_select on document_shares
541
+ for select using (has_capability(company_id, 'documents.read'));
542
+
543
+ comment on policy document_shares_select on document_shares is
544
+ 'Anyone who may read the documents of the company may see which of them are published, and since when. The token is not here: only its hash is.';
545
+
546
+ -- ---------------------------------------------------------------------------
547
+ -- 8. Grants
548
+ --
549
+ -- The rule of `supabase/migrations/README.md`: the schema closes behind itself
550
+ -- and then says, by name, who may reach what. `anon` gains exactly one
551
+ -- execute, on the function that is the public door, and nothing on the table.
552
+ -- ---------------------------------------------------------------------------
553
+
554
+ revoke execute on all functions in schema public from public;
555
+
556
+ grant select on table document_shares to authenticated, service_role;
557
+
558
+ revoke execute on function document_share_refusal(documents) from public, anon;
559
+ grant execute on function document_share_refusal(documents) to authenticated, service_role;
560
+
561
+ revoke execute on function share_document(uuid, timestamptz) from public, anon;
562
+ grant execute on function share_document(uuid, timestamptz) to authenticated, service_role;
563
+
564
+ revoke execute on function revoke_share(uuid) from public, anon;
565
+ grant execute on function revoke_share(uuid) to authenticated, service_role;
566
+
567
+ revoke execute on function shared_document(text) from public;
568
+ grant execute on function shared_document(text) to anon, authenticated, service_role;