shutro-erp 0.3.1 → 0.3.3

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 (168) hide show
  1. package/dist/account_portal/.kosh/bindings.js +1 -1
  2. package/dist/account_portal/.kosh/schema.d.ts +29 -1
  3. package/dist/account_portal/index.js +3 -2
  4. package/dist/accounting/.kosh/bindings.js +1 -1
  5. package/dist/accounting/.kosh/schema.d.ts +29 -1
  6. package/dist/accounting/index.js +4 -0
  7. package/dist/assistant/index.js +1 -1
  8. package/dist/cli/init.js +2 -0
  9. package/dist/cli/launch.js +8 -5
  10. package/dist/cli/templates.js +4 -1
  11. package/dist/cli/ui.js +19 -1
  12. package/dist/contacts/.kosh/bindings.js +1 -1
  13. package/dist/contacts/.kosh/schema.d.ts +29 -1
  14. package/dist/integrations/inbound.d.ts +36 -0
  15. package/dist/integrations/inbound.js +171 -0
  16. package/dist/integrations/index.d.ts +18 -0
  17. package/dist/integrations/index.js +400 -0
  18. package/dist/integrations/models.d.ts +6 -0
  19. package/dist/integrations/models.js +119 -0
  20. package/dist/integrations/web/DeliveryTools.d.ts +4 -0
  21. package/dist/integrations/web/DeliveryTools.js +15 -0
  22. package/dist/integrations/web/EndpointTools.d.ts +7 -0
  23. package/dist/integrations/web/EndpointTools.js +25 -0
  24. package/dist/integrations/web/InboundTools.d.ts +4 -0
  25. package/dist/integrations/web/InboundTools.js +15 -0
  26. package/dist/integrations/web/WebhookTools.d.ts +7 -0
  27. package/dist/integrations/web/WebhookTools.js +25 -0
  28. package/dist/integrations/web/index.d.ts +16 -0
  29. package/dist/integrations/web/index.js +27 -0
  30. package/dist/integrations/web/messages.d.ts +2 -0
  31. package/dist/integrations/web/messages.js +53 -0
  32. package/dist/integrations/webhooks.d.ts +63 -0
  33. package/dist/integrations/webhooks.js +217 -0
  34. package/dist/l10n_bd/.kosh/bindings.js +1 -1
  35. package/dist/l10n_bd/.kosh/schema.d.ts +29 -1
  36. package/dist/l10n_in/.kosh/bindings.js +1 -1
  37. package/dist/l10n_in/.kosh/schema.d.ts +29 -4
  38. package/dist/l10n_in/module.d.ts +0 -62
  39. package/dist/l10n_in/module.js +2 -4
  40. package/dist/modules-sdk/index.d.ts +1 -1
  41. package/dist/modules-sdk/manifest.d.ts +48 -0
  42. package/dist/payment/.kosh/bindings.d.ts +22 -0
  43. package/dist/payment/.kosh/bindings.js +15 -0
  44. package/dist/payment/.kosh/schema.d.ts +1581 -0
  45. package/dist/payment/bkash.d.ts +28 -0
  46. package/dist/payment/bkash.js +169 -0
  47. package/dist/payment/index.d.ts +38 -0
  48. package/dist/payment/index.js +431 -0
  49. package/dist/payment/module.d.ts +6856 -0
  50. package/dist/payment/module.js +77 -0
  51. package/dist/payment/money.d.ts +7 -0
  52. package/dist/payment/money.js +37 -0
  53. package/dist/payment/providers.d.ts +75 -0
  54. package/dist/payment/providers.js +77 -0
  55. package/dist/payment/razorpay.d.ts +12 -0
  56. package/dist/payment/razorpay.js +149 -0
  57. package/dist/payment/settle.d.ts +11 -0
  58. package/dist/payment/settle.js +59 -0
  59. package/dist/payment/sslcommerz.d.ts +12 -0
  60. package/dist/payment/sslcommerz.js +185 -0
  61. package/dist/payment/stripe.d.ts +15 -0
  62. package/dist/payment/stripe.js +182 -0
  63. package/dist/permissions/admin.d.ts +3 -0
  64. package/dist/permissions/admin.js +108 -10
  65. package/dist/permissions/index.d.ts +1 -0
  66. package/dist/permissions/index.js +3 -0
  67. package/dist/permissions/roles.d.ts +2 -0
  68. package/dist/permissions/roles.js +236 -0
  69. package/dist/portal/.kosh/bindings.js +1 -1
  70. package/dist/portal/.kosh/schema.d.ts +29 -1
  71. package/dist/portal/routes-documents.js +5 -0
  72. package/dist/product/.kosh/bindings.js +1 -1
  73. package/dist/product/.kosh/schema.d.ts +29 -1
  74. package/dist/project/.kosh/bindings.js +1 -1
  75. package/dist/project/.kosh/schema.d.ts +29 -1
  76. package/dist/project_portal/.kosh/bindings.js +1 -1
  77. package/dist/project_portal/.kosh/schema.d.ts +29 -1
  78. package/dist/purchase/.kosh/bindings.js +1 -1
  79. package/dist/purchase/.kosh/schema.d.ts +29 -1
  80. package/dist/purchase_account/.kosh/bindings.js +1 -1
  81. package/dist/purchase_account/.kosh/schema.d.ts +29 -1
  82. package/dist/purchase_portal/.kosh/bindings.js +1 -1
  83. package/dist/purchase_portal/.kosh/schema.d.ts +29 -1
  84. package/dist/sale_account/.kosh/bindings.js +1 -1
  85. package/dist/sale_account/.kosh/schema.d.ts +29 -1
  86. package/dist/sale_portal/.kosh/bindings.js +1 -1
  87. package/dist/sale_portal/.kosh/schema.d.ts +29 -1
  88. package/dist/sale_project/.kosh/bindings.js +1 -1
  89. package/dist/sale_project/.kosh/schema.d.ts +29 -1
  90. package/dist/sales/.kosh/bindings.js +1 -1
  91. package/dist/sales/.kosh/schema.d.ts +29 -1
  92. package/dist/sales/index.js +2 -0
  93. package/dist/server/api/rest.d.ts +6 -1
  94. package/dist/server/api/rest.js +11 -0
  95. package/dist/server/auth/base.js +22 -0
  96. package/dist/server/auth/routes.d.ts +3 -0
  97. package/dist/server/auth/routes.js +29 -0
  98. package/dist/server/auth/service.d.ts +52 -0
  99. package/dist/server/auth/service.js +168 -3
  100. package/dist/server/identity.d.ts +2 -0
  101. package/dist/server/index.d.ts +2 -2
  102. package/dist/server/index.js +1 -1
  103. package/dist/server/server.js +11 -3
  104. package/dist/settings/company.d.ts +34 -0
  105. package/dist/settings/company.js +187 -0
  106. package/dist/settings/index.d.ts +10 -10
  107. package/dist/settings/index.js +159 -30
  108. package/dist/settings/people.d.ts +16 -0
  109. package/dist/settings/people.js +505 -0
  110. package/dist/settings/security.d.ts +13 -0
  111. package/dist/settings/security.js +158 -0
  112. package/dist/settings/web/AuditPage.d.ts +8 -0
  113. package/dist/settings/web/AuditPage.js +74 -0
  114. package/dist/settings/web/CompaniesPage.d.ts +8 -0
  115. package/dist/settings/web/CompaniesPage.js +52 -0
  116. package/dist/settings/web/CompanyPage.d.ts +9 -0
  117. package/dist/settings/web/CompanyPage.js +94 -0
  118. package/dist/settings/web/OverviewPage.d.ts +9 -0
  119. package/dist/settings/web/OverviewPage.js +66 -0
  120. package/dist/settings/web/PeoplePage.d.ts +9 -0
  121. package/dist/settings/web/PeoplePage.js +163 -0
  122. package/dist/settings/web/ProfilePage.d.ts +14 -0
  123. package/dist/settings/web/ProfilePage.js +194 -0
  124. package/dist/settings/web/RolesEditor.d.ts +35 -0
  125. package/dist/settings/web/RolesEditor.js +49 -0
  126. package/dist/settings/web/RolesPage.d.ts +9 -0
  127. package/dist/settings/web/RolesPage.js +296 -0
  128. package/dist/settings/web/SecurityPage.d.ts +8 -0
  129. package/dist/settings/web/SecurityPage.js +81 -0
  130. package/dist/settings/web/index.d.ts +9 -0
  131. package/dist/settings/web/index.js +26 -1
  132. package/dist/settings/web/messages.js +566 -0
  133. package/dist/ui/api.d.ts +4 -0
  134. package/dist/ui/app.d.ts +5 -0
  135. package/dist/ui/app.js +108 -31
  136. package/dist/ui/components/primitives.d.ts +9 -1
  137. package/dist/ui/components/primitives.js +12 -4
  138. package/dist/ui/context.d.ts +10 -1
  139. package/dist/ui/context.js +60 -2
  140. package/dist/ui/i18n/messages.d.ts +46 -0
  141. package/dist/ui/i18n/messages.js +92 -0
  142. package/dist/ui/index.d.ts +2 -1
  143. package/dist/ui/index.js +2 -1
  144. package/dist/ui/lib/accent.d.ts +13 -0
  145. package/dist/ui/lib/accent.js +36 -0
  146. package/dist/ui/shell/Activate.d.ts +6 -0
  147. package/dist/ui/shell/Activate.js +39 -0
  148. package/dist/ui/shell/CommandPalette.d.ts +7 -1
  149. package/dist/ui/shell/CommandPalette.js +16 -2
  150. package/dist/ui/shell/Sidebar.d.ts +27 -0
  151. package/dist/ui/shell/Sidebar.js +84 -0
  152. package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
  153. package/dist/ui/shell/TwoFactorSetup.js +32 -0
  154. package/dist/ui/shell/shortcuts.d.ts +3 -0
  155. package/dist/ui/shell/shortcuts.js +13 -4
  156. package/dist/ui/styles.css +70 -28
  157. package/dist/ui/views/DateField.d.ts +10 -0
  158. package/dist/ui/views/DateField.js +129 -0
  159. package/dist/ui/views/FormView.js +161 -21
  160. package/dist/ui/views/ListView.d.ts +2 -0
  161. package/dist/ui/views/ListView.js +8 -3
  162. package/dist/ui/views/StatusChip.d.ts +10 -0
  163. package/dist/ui/views/StatusChip.js +22 -0
  164. package/dist/ui/views/widgets.d.ts +3 -0
  165. package/dist/ui/views/widgets.js +26 -8
  166. package/dist/views/types.d.ts +7 -0
  167. package/dist/views/validate.js +4 -0
  168. package/package.json +14 -2
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 0ea5d21ac5b24166
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 668898d9db81475e
2
2
  // Bindings of module `account_portal`: import the definition API from here to get types scoped to what it can see.
3
3
  export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
4
4
  import { definitions } from '../../server/orm.js';
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 0ea5d21ac5b24166
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 668898d9db81475e
2
2
  // Schema view of module `account_portal` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
3
3
  import type { F } from '../../server/orm.js';
4
4
 
@@ -24,6 +24,7 @@ export interface SchemaView {
24
24
  readonly 'audit.event': AuditEvent;
25
25
  readonly 'auth.api_key': AuthApiKey;
26
26
  readonly 'auth.credential': AuthCredential;
27
+ readonly 'auth.policy': AuthPolicy;
27
28
  readonly 'auth.session': AuthSession;
28
29
  readonly 'auth.user_grant': AuthUserGrant;
29
30
  readonly 'auth.user_group': AuthUserGroup;
@@ -634,6 +635,26 @@ export interface AuthCredential {
634
635
  export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
635
636
  }
636
637
 
638
+ /** `auth.policy` (owner: base). */
639
+ export interface AuthPolicy {
640
+ readonly name: 'auth.policy';
641
+ readonly fields: {
642
+ readonly id: F.Id<'auth.policy'>;
643
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
644
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
645
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
646
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
647
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
648
+ readonly twoFactor: F.Selection<'administrators' | 'everyone', { readonly required: true; readonly default: true }>;
649
+ readonly sessionDays: F.Integer;
650
+ };
651
+ readonly methods: AuthPolicyMethods;
652
+ }
653
+
654
+ /** Methods of `auth.policy`: declared ones and transitions. */
655
+ export interface AuthPolicyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.policy'> {
656
+ }
657
+
637
658
  /** `auth.session` (owner: base). */
638
659
  export interface AuthSession {
639
660
  readonly name: 'auth.session';
@@ -1200,6 +1221,13 @@ export interface ResCompany {
1200
1221
  readonly name: F.Char<{ readonly required: true }>;
1201
1222
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
1202
1223
  readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
1224
+ readonly accentColor: F.Char;
1225
+ readonly street: F.Char;
1226
+ readonly city: F.Char;
1227
+ readonly zip: F.Char;
1228
+ readonly email: F.Char;
1229
+ readonly phone: F.Char;
1230
+ readonly website: F.Char;
1203
1231
  readonly fiscalYearStart: F.Integer<{ readonly required: true; readonly default: true }>; // accounting
1204
1232
  readonly chartTemplate: F.Char; // accounting
1205
1233
  readonly country: F.Char; // accounting
@@ -2,8 +2,8 @@
2
2
  * @packageDocumentation
3
3
  * `shutro-erp/account_portal`: Accounting in the portal (RFC 0015 §5.4). Proposed whenever Accounting and the portal
4
4
  * are installed. Customers see their posted invoices and credit notes, what is still due, print them and write on
5
- * them; vendors see the bills recorded for them. Payment is by the details on the invoice (online payment comes
6
- * with the integrations RFC).
5
+ * them; vendors see the bills recorded for them. Open invoices are payable online when the `payment` module and
6
+ * a provider are installed (RFC 0017 §5.5); otherwise by the payment details on the invoice.
7
7
  */
8
8
  import { accounting } from '../accounting/index.js';
9
9
  import { defineModule } from '../modules-sdk/index.js';
@@ -55,6 +55,7 @@ export const accountPortal = defineModule({
55
55
  thread: true,
56
56
  attention: { kind: 'out_invoice', paymentState: { in: ['not_paid', 'partial'] } },
57
57
  announce: { state: 'posted' },
58
+ payment: { when: { kind: 'out_invoice', paymentState: { in: ['not_paid', 'partial'] } } },
58
59
  },
59
60
  {
60
61
  key: 'bills',
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: c83d584195f8dd43
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: e7d2cbddec9ade69
2
2
  // Bindings of module `accounting`: import the definition API from here to get types scoped to what it can see.
3
3
  export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
4
4
  import { definitions } from '../../server/orm.js';
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: c83d584195f8dd43
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: e7d2cbddec9ade69
2
2
  // Schema view of module `accounting` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
3
3
  import type { F } from '../../server/orm.js';
4
4
 
@@ -24,6 +24,7 @@ export interface SchemaView {
24
24
  readonly 'audit.event': AuditEvent;
25
25
  readonly 'auth.api_key': AuthApiKey;
26
26
  readonly 'auth.credential': AuthCredential;
27
+ readonly 'auth.policy': AuthPolicy;
27
28
  readonly 'auth.session': AuthSession;
28
29
  readonly 'auth.user_grant': AuthUserGrant;
29
30
  readonly 'auth.user_group': AuthUserGroup;
@@ -631,6 +632,26 @@ export interface AuthCredential {
631
632
  export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
632
633
  }
633
634
 
635
+ /** `auth.policy` (owner: base). */
636
+ export interface AuthPolicy {
637
+ readonly name: 'auth.policy';
638
+ readonly fields: {
639
+ readonly id: F.Id<'auth.policy'>;
640
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
641
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
642
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
643
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
644
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
645
+ readonly twoFactor: F.Selection<'administrators' | 'everyone', { readonly required: true; readonly default: true }>;
646
+ readonly sessionDays: F.Integer;
647
+ };
648
+ readonly methods: AuthPolicyMethods;
649
+ }
650
+
651
+ /** Methods of `auth.policy`: declared ones and transitions. */
652
+ export interface AuthPolicyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.policy'> {
653
+ }
654
+
634
655
  /** `auth.session` (owner: base). */
635
656
  export interface AuthSession {
636
657
  readonly name: 'auth.session';
@@ -1125,6 +1146,13 @@ export interface ResCompany {
1125
1146
  readonly name: F.Char<{ readonly required: true }>;
1126
1147
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
1127
1148
  readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
1149
+ readonly accentColor: F.Char;
1150
+ readonly street: F.Char;
1151
+ readonly city: F.Char;
1152
+ readonly zip: F.Char;
1153
+ readonly email: F.Char;
1154
+ readonly phone: F.Char;
1155
+ readonly website: F.Char;
1128
1156
  readonly fiscalYearStart: F.Integer<{ readonly required: true; readonly default: true }>; // accounting
1129
1157
  readonly chartTemplate: F.Char; // accounting
1130
1158
  readonly country: F.Char; // accounting
@@ -404,6 +404,10 @@ export const accounting = defineModule({
404
404
  sequence: 5,
405
405
  },
406
406
  ],
407
+ // What webhooks may subscribe to besides record changes (RFC 0017 §5.3).
408
+ events: [
409
+ { topic: 'account.move.posted', model: 'account.move', label: 'Entry, invoice or bill posted' },
410
+ ],
407
411
  approvals: [
408
412
  {
409
413
  name: 'Vendor bill approvals',
@@ -42,7 +42,7 @@ export function createAssistant(options = {}) {
42
42
  menus: [
43
43
  {
44
44
  id: 'assistant.settings',
45
- parent: 'settings.root',
45
+ parent: 'settings.group_apps',
46
46
  label: 'AI assistant',
47
47
  action: 'assistant.action_settings',
48
48
  sequence: 50,
package/dist/cli/init.js CHANGED
@@ -503,6 +503,7 @@ async function modulesFor(apps) {
503
503
  (await import('../notify/index.js')).notify,
504
504
  (await import('../mail/index.js')).mail,
505
505
  (await import('../automation/index.js')).automation,
506
+ (await import('../integrations/index.js')).integrations,
506
507
  (await import('../studio/index.js')).studio,
507
508
  (await import('../import/index.js')).dataImport,
508
509
  ];
@@ -940,6 +941,7 @@ export async function runInit(argv, io) {
940
941
  'notify',
941
942
  'mail',
942
943
  'automation',
944
+ 'integrations',
943
945
  'studio',
944
946
  'import',
945
947
  ...answers.apps,
@@ -24,14 +24,17 @@ export function browserCommand(url, platform = process.platform) {
24
24
  const windows = process.platform === 'win32';
25
25
  /** A child process running `pm args` in `cwd`, its output kept (the last 200 lines). */
26
26
  function start(pm, args, cwd) {
27
- const child = spawn(pm, args, {
27
+ const options = {
28
28
  cwd,
29
29
  env: { ...process.env, FORCE_COLOR: '0' },
30
30
  stdio: ['ignore', 'pipe', 'pipe'],
31
- // npm, pnpm… are .cmd scripts on Windows; elsewhere a process group, stopped as a whole.
32
- shell: windows,
33
- detached: !windows,
34
- });
31
+ detached: !windows, // elsewhere a process group, stopped as a whole
32
+ };
33
+ // npm, pnpm… are .cmd scripts on Windows: run through the shell as one command line (the arguments are this
34
+ // module's own words, never input), which Node prefers to separate arguments with `shell: true`.
35
+ const child = windows
36
+ ? spawn([pm, ...args].join(' '), { ...options, shell: true })
37
+ : spawn(pm, args, options);
35
38
  const lines = [];
36
39
  const keep = (chunk) => {
37
40
  lines.push(...chunk
@@ -88,6 +88,7 @@ const REACT = {
88
88
  react: '^19.1.0',
89
89
  'react-dom': '^19.1.0',
90
90
  '@tanstack/react-query': '^5.90.0',
91
+ '@tanstack/react-router': '^1.170.0',
91
92
  i18next: '^25.0.0 || ^26.0.0',
92
93
  'react-i18next': '^16.0.0 || ^17.0.0',
93
94
  };
@@ -136,6 +137,7 @@ export function modulesTs(_options) {
136
137
  */
137
138
  import { base } from 'shutro-erp';
138
139
  import { automation } from 'shutro-erp/automation';
140
+ import { integrations } from 'shutro-erp/integrations';
139
141
  import { mail } from 'shutro-erp/mail';
140
142
  import { notify } from 'shutro-erp/notify';
141
143
  import { realtime } from 'shutro-erp/realtime';
@@ -145,7 +147,7 @@ import { dataImport } from 'shutro-erp/import';
145
147
  import { studio } from 'shutro-erp/studio';
146
148
  ${imports.join('\n')}
147
149
 
148
- export const modules = [base, settings, realtime, notify, mail, automation, studio, dataImport, assistant${apps.map((a) => `, ${APPS[a].server}`).join('')}];
150
+ export const modules = [base, settings, realtime, notify, mail, automation, integrations, studio, dataImport, assistant${apps.map((a) => `, ${APPS[a].server}`).join('')}];
149
151
  `;
150
152
  }
151
153
  export function serverTs() {
@@ -216,6 +218,7 @@ export function mainTsx(_options) {
216
218
  { name: 'notify', fn: 'registerNotifyWeb' },
217
219
  { name: 'mail', fn: 'registerMailWeb' },
218
220
  { name: 'automation', fn: 'registerAutomationWeb' },
221
+ { name: 'integrations', fn: 'registerIntegrationsWeb' },
219
222
  { name: 'studio', fn: 'registerStudioWeb' },
220
223
  { name: 'assistant', fn: 'registerAssistantWeb' },
221
224
  { name: 'import', fn: 'registerImportWeb' },
package/dist/cli/ui.js CHANGED
@@ -251,7 +251,25 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
251
251
  },
252
252
  card(title, rows, tone = 'green') {
253
253
  const labelWidth = Math.max(0, ...rows.map(([l]) => width(l)));
254
- const body = rows.map(([l, v]) => `${s('gray', l.padEnd(labelWidth))} ${v}`);
254
+ // Long values wrap at the terminal's width, aligned after their label.
255
+ const room = Math.max(20, Math.min(terminal.columns, 100) - labelWidth - 10);
256
+ const wrap = (value) => {
257
+ const lines = [];
258
+ let current = '';
259
+ for (const word of value.split(' ')) {
260
+ const next = current === '' ? word : `${current} ${word}`;
261
+ if (current !== '' && width(next) > room) {
262
+ lines.push(current);
263
+ current = word;
264
+ }
265
+ else
266
+ current = next;
267
+ }
268
+ return [...lines, current];
269
+ };
270
+ const body = rows.flatMap(([l, v]) => wrap(v).map((part, i) => i === 0
271
+ ? `${s('gray', l.padEnd(labelWidth))} ${part}`
272
+ : `${' '.repeat(labelWidth)} ${part}`));
255
273
  const inner = Math.max(width(title), ...body.map(width)) + 2;
256
274
  const [tl, tr, bl, br, h, v] = terminal.unicode
257
275
  ? ['╭', '╮', '╰', '╯', '─', '│']
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 31cf57953410c779
2
2
  // Bindings of module `contacts`: import the definition API from here to get types scoped to what it can see.
3
3
  export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
4
4
  import { definitions } from '../../server/orm.js';
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 31cf57953410c779
2
2
  // Schema view of module `contacts` (RFC 0002 §7.7): the models it owns or depends on, with the extensions it can see.
3
3
  import type { F } from '../../server/orm.js';
4
4
 
@@ -7,6 +7,7 @@ export interface SchemaView {
7
7
  readonly 'audit.event': AuditEvent;
8
8
  readonly 'auth.api_key': AuthApiKey;
9
9
  readonly 'auth.credential': AuthCredential;
10
+ readonly 'auth.policy': AuthPolicy;
10
11
  readonly 'auth.session': AuthSession;
11
12
  readonly 'auth.user_grant': AuthUserGrant;
12
13
  readonly 'auth.user_group': AuthUserGroup;
@@ -94,6 +95,26 @@ export interface AuthCredential {
94
95
  export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
95
96
  }
96
97
 
98
+ /** `auth.policy` (owner: base). */
99
+ export interface AuthPolicy {
100
+ readonly name: 'auth.policy';
101
+ readonly fields: {
102
+ readonly id: F.Id<'auth.policy'>;
103
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
104
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
105
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
106
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
107
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
108
+ readonly twoFactor: F.Selection<'administrators' | 'everyone', { readonly required: true; readonly default: true }>;
109
+ readonly sessionDays: F.Integer;
110
+ };
111
+ readonly methods: AuthPolicyMethods;
112
+ }
113
+
114
+ /** Methods of `auth.policy`: declared ones and transitions. */
115
+ export interface AuthPolicyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.policy'> {
116
+ }
117
+
97
118
  /** `auth.session` (owner: base). */
98
119
  export interface AuthSession {
99
120
  readonly name: 'auth.session';
@@ -203,6 +224,13 @@ export interface ResCompany {
203
224
  readonly name: F.Char<{ readonly required: true }>;
204
225
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
205
226
  readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
227
+ readonly accentColor: F.Char;
228
+ readonly street: F.Char;
229
+ readonly city: F.Char;
230
+ readonly zip: F.Char;
231
+ readonly email: F.Char;
232
+ readonly phone: F.Char;
233
+ readonly website: F.Char;
206
234
  };
207
235
  readonly methods: ResCompanyMethods;
208
236
  }
@@ -0,0 +1,36 @@
1
+ import type { InboundCall, InboundWebhookSpec } from '../modules-sdk/index.js';
2
+ import type { KoshRegistry } from '../server/index.js';
3
+ import type { Shutro } from '../server/orm.js';
4
+ export declare const INBOUND_JOB = "webhook.inbound";
5
+ /** The declared inbound webhooks of the loaded modules, by `<module>.<key>`. */
6
+ export declare function inboundHooks(registry: KoshRegistry): Map<string, InboundWebhookSpec & {
7
+ readonly module: string;
8
+ }>;
9
+ /**
10
+ * A `verify` for providers that sign with an HMAC of the raw body: the signature in `header` (hex or base64,
11
+ * optionally after a prefix such as `sha256=`), compared in constant time; the event id from `eventId`.
12
+ */
13
+ export declare function hmacVerifier(options: {
14
+ readonly header: string;
15
+ readonly algorithm?: 'sha256' | 'sha512' | 'sha1';
16
+ readonly encoding?: 'hex' | 'base64';
17
+ readonly prefix?: string;
18
+ readonly eventId: (call: InboundCall) => string | null | undefined;
19
+ }): (call: InboundCall, secret: string) => string | null;
20
+ export interface ReceiveResult {
21
+ readonly status: 202 | 200 | 401 | 404;
22
+ readonly body: Record<string, unknown>;
23
+ }
24
+ /** Receives a call on the endpoint of `token`: checked, stored, queued once per event id. */
25
+ export declare function receive(context: {
26
+ readonly shutro: Shutro;
27
+ readonly registry: KoshRegistry;
28
+ }, token: string, call: InboundCall): Promise<ReceiveResult>;
29
+ /**
30
+ * Handles a stored call with its module's `handle`, as the system in the endpoint's company; records the outcome
31
+ * in its own transaction. Throws to be retried while attempts remain.
32
+ */
33
+ export declare function handleInbound(context: {
34
+ readonly shutro: Shutro;
35
+ readonly registry: KoshRegistry;
36
+ }, callId: number, company: number, last: boolean): Promise<void>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Inbound webhooks (RFC 0017 §5.4). Modules declare endpoints (`inboundWebhooks` in their manifest); a company
3
+ * switches one on (`webhook.endpoint`: a URL token and a secret). A call to `POST /api/v1/hooks/<token>` is checked
4
+ * by the module's `verify` against the raw body, stored (refused calls too, for the log), and — once per event id —
5
+ * handled by the module in a job, as the system in the endpoint's company, retried on failure. A repeated event id
6
+ * is answered 200 and not handled again; a call can be handled again from the log (replay).
7
+ */
8
+ import { createHmac, timingSafeEqual } from 'node:crypto';
9
+ import { ref } from './webhooks.js';
10
+ export const INBOUND_JOB = 'webhook.inbound';
11
+ /** The declared inbound webhooks of the loaded modules, by `<module>.<key>`. */
12
+ export function inboundHooks(registry) {
13
+ const out = new Map();
14
+ for (const m of registry.loaded.modules)
15
+ for (const hook of m.inboundWebhooks ?? [])
16
+ out.set(`${m.name}.${hook.key}`, { ...hook, module: m.name });
17
+ return out;
18
+ }
19
+ /**
20
+ * A `verify` for providers that sign with an HMAC of the raw body: the signature in `header` (hex or base64,
21
+ * optionally after a prefix such as `sha256=`), compared in constant time; the event id from `eventId`.
22
+ */
23
+ export function hmacVerifier(options) {
24
+ return (call, secret) => {
25
+ let given = call.headers[options.header.toLowerCase()] ?? '';
26
+ if (options.prefix !== undefined) {
27
+ if (!given.startsWith(options.prefix))
28
+ return null;
29
+ given = given.slice(options.prefix.length);
30
+ }
31
+ const expected = createHmac(options.algorithm ?? 'sha256', secret)
32
+ .update(call.body)
33
+ .digest(options.encoding ?? 'hex');
34
+ const a = Buffer.from(given);
35
+ const b = Buffer.from(expected);
36
+ if (a.length !== b.length || !timingSafeEqual(a, b))
37
+ return null;
38
+ const id = options.eventId(call);
39
+ return id === null || id === undefined || id === '' ? null : String(id);
40
+ };
41
+ }
42
+ /** Headers worth keeping in the log (no cookies, no credentials). */
43
+ const KEPT = /^(content-type|user-agent|x-[a-z0-9-]+)$/;
44
+ function keptHeaders(headers) {
45
+ return Object.fromEntries(Object.entries(headers).filter((e) => e[1] !== undefined && KEPT.test(e[0]) && !/authorization|api-key|cookie/.test(e[0])));
46
+ }
47
+ /** Receives a call on the endpoint of `token`: checked, stored, queued once per event id. */
48
+ export async function receive(context, token, call) {
49
+ const hooks = inboundHooks(context.registry);
50
+ // Which endpoint (and company) the token names: looked up across companies (the token is unique).
51
+ const found = await context.shutro.bootstrap({ reason: 'inbound webhook: endpoint' }, (env) => env.asSuperuser({ reason: 'inbound webhook: which endpoint' }, async (su) => {
52
+ const set = await ref(su.withFlags({ checkCompany: false }), 'webhook.endpoint').search({
53
+ where: { token, active: true },
54
+ limit: 1,
55
+ });
56
+ if (set.length === 0)
57
+ return null;
58
+ const [row] = (await set.load(['hook', 'secret', 'company'])).read([
59
+ 'id',
60
+ 'hook',
61
+ 'secret',
62
+ 'company',
63
+ ]);
64
+ return row ?? null;
65
+ }));
66
+ const hook = found === null ? undefined : hooks.get(String(found['hook']));
67
+ if (found === null || hook === undefined)
68
+ return { status: 404, body: { title: 'Not found', detail: 'No such webhook endpoint.' } };
69
+ const company = found['company'];
70
+ let eventId = null;
71
+ try {
72
+ eventId = await hook.verify(call, String(found['secret']));
73
+ }
74
+ catch {
75
+ eventId = null;
76
+ }
77
+ return context.shutro.bootstrap({ reason: `inbound webhook ${hook.module}.${hook.key}`, company }, async (env) => {
78
+ const calls = ref(env, 'webhook.inbound');
79
+ const base = {
80
+ endpoint: found['id'],
81
+ headers: keptHeaders(call.headers),
82
+ body: call.body.slice(0, 256 * 1024),
83
+ company,
84
+ };
85
+ if (eventId === null) {
86
+ await calls.create([{ ...base, verified: false, state: 'refused' }]);
87
+ return {
88
+ status: 401,
89
+ body: { title: 'Not verified', detail: 'The call’s signature does not match.' },
90
+ };
91
+ }
92
+ const seen = await calls.search({
93
+ where: { endpoint: found['id'], eventId, verified: true },
94
+ limit: 1,
95
+ });
96
+ if (seen.length > 0)
97
+ return { status: 200, body: { duplicate: true, eventId } };
98
+ const created = await calls.create([{ ...base, eventId, verified: true, state: 'received' }]);
99
+ const id = created.ids[0];
100
+ await env.enqueue(INBOUND_JOB, { call: id, company }, { attempts: 6 });
101
+ return { status: 202, body: { received: true, eventId } };
102
+ });
103
+ }
104
+ /**
105
+ * Handles a stored call with its module's `handle`, as the system in the endpoint's company; records the outcome
106
+ * in its own transaction. Throws to be retried while attempts remain.
107
+ */
108
+ export async function handleInbound(context, callId, company, last) {
109
+ const hooks = inboundHooks(context.registry);
110
+ const system = (reason, fn) => context.shutro.bootstrap({ reason, company }, fn);
111
+ const found = await system('inbound webhook: the call', async (env) => {
112
+ const set = ref(env, 'webhook.inbound').browse([callId]);
113
+ const [c] = (await set.load(['endpoint', 'body', 'headers', 'state', 'attempts'])).read([
114
+ 'endpoint',
115
+ 'body',
116
+ 'headers',
117
+ 'state',
118
+ 'attempts',
119
+ ]);
120
+ if (c === undefined)
121
+ return null;
122
+ const [e] = (await ref(env, 'webhook.endpoint')
123
+ .browse([c['endpoint']])
124
+ .load(['hook'])).read(['hook']);
125
+ return { row: c, hook: String(e?.['hook'] ?? '') };
126
+ });
127
+ if (found === null || found.row['state'] === 'done')
128
+ return;
129
+ const { row, hook: name } = found;
130
+ const hook = hooks.get(name);
131
+ const attempts = Number(row['attempts'] ?? 0) + 1;
132
+ const write = (values) => system('inbound webhook: the outcome', (env) => ref(env, 'webhook.inbound').browse([callId]).write(values));
133
+ if (hook === undefined) {
134
+ await write({
135
+ state: 'failed',
136
+ attempts,
137
+ error: `The endpoint ${name} is no longer installed.`,
138
+ });
139
+ return;
140
+ }
141
+ const body = String(row['body']);
142
+ let json = null;
143
+ try {
144
+ json = JSON.parse(body);
145
+ }
146
+ catch {
147
+ json = null;
148
+ }
149
+ const call = {
150
+ headers: (row['headers'] ?? {}),
151
+ query: {},
152
+ body,
153
+ json,
154
+ };
155
+ try {
156
+ const result = await system(`inbound webhook ${name}`, (env) => hook.handle(env, call));
157
+ await write({
158
+ state: 'done',
159
+ attempts,
160
+ result: typeof result === 'string' ? result : null,
161
+ error: null,
162
+ handledAt: new Date(),
163
+ });
164
+ }
165
+ catch (error) {
166
+ const message = error instanceof Error ? error.message : String(error);
167
+ await write({ state: last ? 'failed' : 'received', attempts, error: message });
168
+ if (!last)
169
+ throw error;
170
+ }
171
+ }
@@ -0,0 +1,18 @@
1
+ import { type KoshModule } from '../modules-sdk/index.js';
2
+ import type { Worker } from '../server/orm.js';
3
+ import { type IntegrationsContext } from './webhooks.js';
4
+ export { handleInbound, hmacVerifier, INBOUND_JOB, inboundHooks, type ReceiveResult, receive, } from './inbound.js';
5
+ export { integrationsShutro, newSecret, newToken } from './models.js';
6
+ export { CHANGE_EVENTS, DELIVER_JOB, deliver, fanOut, type IntegrationsContext, PING_EVENT, signature, urlProblem, type WebhookEvent, webhookEvents, webhookHosts, } from './webhooks.js';
7
+ /** The workers: events to deliveries (change events and the modules' events), and the delivery jobs. */
8
+ export declare function webhookWorkers(integrations: IntegrationsContext): Worker[];
9
+ export interface IntegrationsOptions {
10
+ /** The HTTP client deliveries use (tests). */
11
+ readonly fetch?: typeof fetch;
12
+ /** Start the workers (default: unless `SHUTRO_ERP_SCHEDULES=off`). */
13
+ readonly workers?: boolean;
14
+ }
15
+ /** The Integrations app with explicit options (tests, installations wiring their own). */
16
+ export declare function integrationsModule(options?: IntegrationsOptions): KoshModule;
17
+ /** The Integrations app (providers and workers from the environment). */
18
+ export declare const integrations: KoshModule;