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
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * The web contribution of `shutro-erp/integrations`: the tools on a webhook's form (test event, new secret, the events
4
+ * offered), on a delivery's (send again), on an inbound webhook's (its URL, a new secret) and on an inbound call's
5
+ * (handle again).
6
+ */
7
+ import { type Registry } from '../../ui/index.js';
8
+ import type { i18n } from 'i18next';
9
+ import { messages } from './messages.js';
10
+ export { DeliveryTools } from './DeliveryTools.js';
11
+ export { EndpointTools } from './EndpointTools.js';
12
+ export { InboundTools } from './InboundTools.js';
13
+ export { WebhookTools } from './WebhookTools.js';
14
+ /** Registers the components (and the messages in `i18n`, when given). */
15
+ export declare function registerIntegrationsWeb(registry: Registry, i18n?: i18n): Registry;
16
+ export { messages as integrationsMessages };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * The web contribution of `shutro-erp/integrations`: the tools on a webhook's form (test event, new secret, the events
4
+ * offered), on a delivery's (send again), on an inbound webhook's (its URL, a new secret) and on an inbound call's
5
+ * (handle again).
6
+ */
7
+ import { addMessages } from '../../ui/index.js';
8
+ import { DeliveryTools } from './DeliveryTools.js';
9
+ import { EndpointTools } from './EndpointTools.js';
10
+ import { InboundTools } from './InboundTools.js';
11
+ import { messages } from './messages.js';
12
+ import { WebhookTools } from './WebhookTools.js';
13
+ export { DeliveryTools } from './DeliveryTools.js';
14
+ export { EndpointTools } from './EndpointTools.js';
15
+ export { InboundTools } from './InboundTools.js';
16
+ export { WebhookTools } from './WebhookTools.js';
17
+ /** Registers the components (and the messages in `i18n`, when given). */
18
+ export function registerIntegrationsWeb(registry, i18n) {
19
+ registry.registerComponent('integrations.WebhookTools', WebhookTools);
20
+ registry.registerComponent('integrations.DeliveryTools', DeliveryTools);
21
+ registry.registerComponent('integrations.EndpointTools', EndpointTools);
22
+ registry.registerComponent('integrations.InboundTools', InboundTools);
23
+ if (i18n !== undefined)
24
+ addMessages(i18n, 'integrations', messages);
25
+ return registry;
26
+ }
27
+ export { messages as integrationsMessages };
@@ -0,0 +1,2 @@
1
+ /** Messages of the Integrations app (namespace `integrations`), in English and Bangla. */
2
+ export declare const messages: Readonly<Record<string, object>>;
@@ -0,0 +1,53 @@
1
+ /** Messages of the Integrations app (namespace `integrations`), in English and Bangla. */
2
+ export const messages = {
3
+ en: {
4
+ tools: {
5
+ title: 'Webhook tools',
6
+ ping: 'Send a test event',
7
+ pinged: 'A test event is on its way: see Deliveries.',
8
+ rotate: 'New signing secret',
9
+ events: 'Events you can subscribe to',
10
+ eventsHint: 'Put the events in the Events field, separated by spaces. A record’s payload is what the webhook’s user may read.',
11
+ replay: 'Send again',
12
+ replayed: 'Sent again: a new delivery is in Deliveries.',
13
+ },
14
+ endpoint: {
15
+ title: 'Inbound webhook',
16
+ url: 'URL to give the provider',
17
+ urlHint: 'Calls are checked with the secret; refused ones stay in Inbound calls.',
18
+ rotate: 'New secret',
19
+ hooks: 'Endpoints the installed apps offer',
20
+ hooksHint: 'Put one of them in the Endpoint field.',
21
+ none: 'No installed app offers an inbound webhook.',
22
+ },
23
+ inbound: {
24
+ replay: 'Handle again',
25
+ replayed: 'Queued: the outcome appears here shortly.',
26
+ },
27
+ },
28
+ bn: {
29
+ tools: {
30
+ title: 'ওয়েবহুক সরঞ্জাম',
31
+ ping: 'পরীক্ষামূলক ইভেন্ট পাঠান',
32
+ pinged: 'পরীক্ষামূলক ইভেন্ট পাঠানো হচ্ছে: ডেলিভারি দেখুন।',
33
+ rotate: 'নতুন স্বাক্ষর গোপন কী',
34
+ events: 'যেসব ইভেন্টে সাবস্ক্রাইব করা যায়',
35
+ eventsHint: 'ইভেন্টগুলো ইভেন্ট ঘরে ফাঁকা জায়গা দিয়ে আলাদা করে লিখুন। রেকর্ডের তথ্য ওয়েবহুকের ব্যবহারকারী যা পড়তে পারেন ততটুকুই।',
36
+ replay: 'আবার পাঠান',
37
+ replayed: 'আবার পাঠানো হয়েছে: ডেলিভারিতে নতুন একটি আছে।',
38
+ },
39
+ endpoint: {
40
+ title: 'ইনবাউন্ড ওয়েবহুক',
41
+ url: 'প্রোভাইডারকে দেওয়ার URL',
42
+ urlHint: 'প্রতিটি কল গোপন কী দিয়ে যাচাই হয়; প্রত্যাখ্যাত কল ইনবাউন্ড কলে থাকে।',
43
+ rotate: 'নতুন গোপন কী',
44
+ hooks: 'ইনস্টল করা অ্যাপগুলোর এন্ডপয়েন্ট',
45
+ hooksHint: 'এর একটি এন্ডপয়েন্ট ঘরে লিখুন।',
46
+ none: 'কোনো ইনস্টল করা অ্যাপ ইনবাউন্ড ওয়েবহুক দেয় না।',
47
+ },
48
+ inbound: {
49
+ replay: 'আবার প্রক্রিয়া করুন',
50
+ replayed: 'সারিতে আছে: ফলাফল শীঘ্রই এখানে দেখা যাবে।',
51
+ },
52
+ },
53
+ };
@@ -0,0 +1,63 @@
1
+ import { type AuthService, type KoshRegistry } from '../server/index.js';
2
+ import type { Env, Shutro } from '../server/orm.js';
3
+ export declare const DELIVER_JOB = "webhook.deliver";
4
+ export declare const CHANGE_TOPIC = "record.changed";
5
+ /** Shutro's change actions and the events they are. */
6
+ export declare const CHANGE_EVENTS: Readonly<Record<string, string>>;
7
+ /** The test event a "send a test" sends. */
8
+ export declare const PING_EVENT = "webhook.ping";
9
+ export interface IntegrationsContext {
10
+ readonly shutro: Shutro;
11
+ readonly registry: KoshRegistry;
12
+ readonly auth?: AuthService | undefined;
13
+ /** For tests: the HTTP client deliveries use. */
14
+ readonly fetch?: typeof fetch;
15
+ }
16
+ /** The hosts webhooks may call (`SHUTRO_ERP_WEBHOOK_HOSTS`, else automation's list; `*.example.com` allowed). */
17
+ export declare function webhookHosts(env?: Readonly<Record<string, string | undefined>>): string[];
18
+ /** Why a URL may not be called, or `null`: https (http for localhost), and a host on the allow-list. */
19
+ export declare function urlProblem(url: string, hosts?: readonly string[]): string | null;
20
+ /** `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`: what receivers check (and reject when old). */
21
+ export declare function signature(secret: string, body: string, time: number): string;
22
+ export interface WebhookEvent {
23
+ readonly event: string;
24
+ readonly model: string;
25
+ readonly label: string;
26
+ }
27
+ /** The events subscriptions may want here: each served model's changes, and the modules' declared events. */
28
+ export declare function webhookEvents(registry: KoshRegistry): WebhookEvent[];
29
+ interface Rows {
30
+ readonly ids: readonly number[];
31
+ readonly length: number;
32
+ load(spec: unknown): Promise<Rows>;
33
+ read(keys: readonly string[]): Record<string, unknown>[];
34
+ write(values: unknown): Promise<void>;
35
+ }
36
+ interface Ref {
37
+ search(query: unknown): Promise<Rows>;
38
+ browse(ids: readonly number[]): Rows;
39
+ create(rows: readonly unknown[]): Promise<Rows>;
40
+ }
41
+ export declare const ref: (env: Env, model: string) => Ref;
42
+ /**
43
+ * Turns an event into deliveries for the matching active subscriptions of `company`, one per record, and queues
44
+ * them. Returns the deliveries made.
45
+ */
46
+ export declare function fanOut(context: IntegrationsContext, input: {
47
+ readonly event: string;
48
+ readonly model: string;
49
+ readonly ids: readonly number[];
50
+ readonly company: number;
51
+ readonly extra?: Readonly<Record<string, unknown>>;
52
+ }): Promise<number[]>;
53
+ /** A new delivery of `payload` to a subscription, queued; returns its id. */
54
+ export declare function queue(context: IntegrationsContext, subscription: number, company: number, event: string, payload: Record<string, unknown>, replayOf?: number): Promise<number>;
55
+ /**
56
+ * Sends one delivery and records the attempt (in its own transaction, so a retry keeps it). Throws to be retried
57
+ * while attempts remain; the last failure marks the delivery failed and counts against the subscription.
58
+ */
59
+ export declare function deliver(context: IntegrationsContext, delivery: number, company: number, attempt: {
60
+ readonly number: number;
61
+ readonly last: boolean;
62
+ }): Promise<void>;
63
+ export {};
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Outbound webhooks (RFC 0017 §5.3). Events — a record created, updated or deleted (Shutro's change events), or an
3
+ * event a module declares (`account.move.posted`) — become deliveries for the active subscriptions that want them;
4
+ * each delivery is a job that POSTs `{ id, event, occurredAt, data }`, signed, and records every attempt. A failure
5
+ * is retried with Shutro's back-off; the last one marks the delivery failed. The record in a payload is what the
6
+ * subscription's user would get from `GET /api/v1/models/<model>/<id>`: their rights, their fields.
7
+ */
8
+ import { createHmac, randomUUID } from 'node:crypto';
9
+ import { publicApiModels, readApiRecord, } from '../server/index.js';
10
+ export const DELIVER_JOB = 'webhook.deliver';
11
+ export const CHANGE_TOPIC = 'record.changed';
12
+ /** Shutro's change actions and the events they are. */
13
+ export const CHANGE_EVENTS = {
14
+ create: 'created',
15
+ write: 'updated',
16
+ unlink: 'deleted',
17
+ };
18
+ /** The test event a "send a test" sends. */
19
+ export const PING_EVENT = 'webhook.ping';
20
+ /** The hosts webhooks may call (`SHUTRO_ERP_WEBHOOK_HOSTS`, else automation's list; `*.example.com` allowed). */
21
+ export function webhookHosts(env = process.env) {
22
+ return (env['KOSH_WEBHOOK_HOSTS'] ?? env['KOSH_AUTOMATION_WEBHOOK_HOSTS'] ?? '')
23
+ .split(',')
24
+ .map((h) => h.trim().toLowerCase())
25
+ .filter((h) => h !== '');
26
+ }
27
+ /** Why a URL may not be called, or `null`: https (http for localhost), and a host on the allow-list. */
28
+ export function urlProblem(url, hosts = webhookHosts()) {
29
+ let parsed;
30
+ try {
31
+ parsed = new URL(url);
32
+ }
33
+ catch {
34
+ return `Not a URL: '${url}'.`;
35
+ }
36
+ const host = parsed.hostname.toLowerCase();
37
+ const local = host === 'localhost' || host === '127.0.0.1';
38
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && local))
39
+ return 'Webhooks call https URLs (http only for localhost).';
40
+ if (!hosts.some((h) => (h.startsWith('*.') ? host.endsWith(h.slice(1)) : host === h)))
41
+ return `The host ${host} is not on the webhook allow-list (SHUTRO_ERP_WEBHOOK_HOSTS).`;
42
+ return null;
43
+ }
44
+ /** `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">`: what receivers check (and reject when old). */
45
+ export function signature(secret, body, time) {
46
+ return `t=${time},v1=${createHmac('sha256', secret).update(`${time}.${body}`).digest('hex')}`;
47
+ }
48
+ /** The events subscriptions may want here: each served model's changes, and the modules' declared events. */
49
+ export function webhookEvents(registry) {
50
+ const out = [];
51
+ for (const model of publicApiModels(registry).values())
52
+ for (const [, verb] of Object.entries(CHANGE_EVENTS))
53
+ out.push({
54
+ event: `${model.name}.${verb}`,
55
+ model: model.name,
56
+ label: `${model.label} ${verb}`,
57
+ });
58
+ for (const m of registry.loaded.modules)
59
+ for (const e of m.events ?? [])
60
+ out.push({ event: e.topic, model: e.model, label: e.label });
61
+ return out.sort((a, b) => a.event.localeCompare(b.event));
62
+ }
63
+ export const ref = (env, model) => env.model(model);
64
+ async function rowsOf(set, keys) {
65
+ return set.length === 0 ? [] : (await set.load(keys)).read(['id', ...keys]);
66
+ }
67
+ /** Runs `fn` as `user` in `company` (their groups and grants), for reading what they may see. */
68
+ async function asUser(context, user, company, fn) {
69
+ const identity = await context.auth?.verify(user);
70
+ if (identity === null || identity === undefined || !identity.companies.includes(company))
71
+ return null;
72
+ try {
73
+ return await context.shutro.run({
74
+ user,
75
+ company,
76
+ groups: [...identity.groups],
77
+ grants: [...(identity.grants ?? [])],
78
+ }, fn);
79
+ }
80
+ catch (error) {
81
+ // Their roles do not let them read it: as good as not visible.
82
+ if (error instanceof Error && error.name === 'AccessError')
83
+ return null;
84
+ throw error;
85
+ }
86
+ }
87
+ /** Whether record `id` of `model` still exists (whoever may see it). */
88
+ async function exists(context, model, id, company) {
89
+ return context.shutro.bootstrap({ reason: 'webhooks: still there?', company }, async (env) => env.asSuperuser({ reason: 'webhooks: does the record still exist' }, async (su) => (await ref(su, model).search({ where: { id }, limit: 1 })).length > 0));
90
+ }
91
+ /**
92
+ * Turns an event into deliveries for the matching active subscriptions of `company`, one per record, and queues
93
+ * them. Returns the deliveries made.
94
+ */
95
+ export async function fanOut(context, input) {
96
+ // System work outside any company (installation) has no company's webhooks to tell.
97
+ if (!(Number.isSafeInteger(input.company) && input.company > 0))
98
+ return [];
99
+ const apiModel = publicApiModels(context.registry).get(input.model);
100
+ const subscriptions = await context.shutro.bootstrap({ reason: `webhooks: ${input.event}`, company: input.company }, async (env) => (await rowsOf(await ref(env, 'webhook.subscription').search({
101
+ where: { active: true, company: input.company },
102
+ }), ['events', 'runAs', 'createdBy'])).filter((s) => String(s['events']).split(/\s+/).includes(input.event)));
103
+ if (subscriptions.length === 0)
104
+ return [];
105
+ const made = [];
106
+ for (const sub of subscriptions) {
107
+ const user = (sub['runAs'] ?? sub['createdBy']);
108
+ for (const id of input.ids) {
109
+ let record = null;
110
+ if (!input.event.endsWith('.deleted') && apiModel !== undefined && user !== null) {
111
+ record = await asUser(context, user, input.company, (env) => readApiRecord(env, apiModel, id));
112
+ // Not theirs to see: nothing is sent. Gone since (deleted after this change): sent without the record.
113
+ if (record === null && (await exists(context, input.model, id, input.company)))
114
+ continue;
115
+ }
116
+ const payload = {
117
+ id: randomUUID(),
118
+ event: input.event,
119
+ occurredAt: new Date().toISOString(),
120
+ data: { model: input.model, id, record, ...input.extra },
121
+ };
122
+ made.push(await queue(context, sub['id'], input.company, input.event, payload));
123
+ }
124
+ }
125
+ return made;
126
+ }
127
+ /** A new delivery of `payload` to a subscription, queued; returns its id. */
128
+ export async function queue(context, subscription, company, event, payload, replayOf) {
129
+ return context.shutro.bootstrap({ reason: `webhook delivery ${event}`, company }, async (env) => {
130
+ const created = await ref(env, 'webhook.delivery').create([
131
+ { subscription, event, payload, company, ...(replayOf === undefined ? {} : { replayOf }) },
132
+ ]);
133
+ const id = created.ids[0];
134
+ await env.enqueue(DELIVER_JOB, { delivery: id, company }, { attempts: 8 });
135
+ return id;
136
+ });
137
+ }
138
+ /**
139
+ * Sends one delivery and records the attempt (in its own transaction, so a retry keeps it). Throws to be retried
140
+ * while attempts remain; the last failure marks the delivery failed and counts against the subscription.
141
+ */
142
+ export async function deliver(context, delivery, company, attempt) {
143
+ const system = (fn) => context.shutro.bootstrap({ reason: `webhook delivery ${delivery}`, company }, fn);
144
+ const found = await system(async (env) => {
145
+ const [d] = await rowsOf(ref(env, 'webhook.delivery').browse([delivery]), [
146
+ 'subscription',
147
+ 'event',
148
+ 'payload',
149
+ 'attempts',
150
+ 'state',
151
+ ]);
152
+ if (d === undefined)
153
+ return null;
154
+ const [s] = await rowsOf(ref(env, 'webhook.subscription').browse([d['subscription']]), ['url', 'secret', 'active', 'failures']);
155
+ return s === undefined ? null : { d, s };
156
+ });
157
+ if (found === null || found.d['state'] === 'done')
158
+ return;
159
+ const { d, s } = found;
160
+ const record = (values, subscription) => system(async (env) => {
161
+ await ref(env, 'webhook.delivery').browse([delivery]).write(values);
162
+ if (subscription !== undefined)
163
+ await ref(env, 'webhook.subscription')
164
+ .browse([s['id']])
165
+ .write(subscription);
166
+ });
167
+ const attempts = Number(d['attempts'] ?? 0) + 1;
168
+ const problem = s['active'] !== true ? 'The webhook is switched off.' : urlProblem(String(s['url']));
169
+ if (problem !== null) {
170
+ await record({ state: 'failed', attempts, error: problem });
171
+ return;
172
+ }
173
+ const body = JSON.stringify(d['payload']);
174
+ const payload = d['payload'];
175
+ const started = Date.now();
176
+ let status = null;
177
+ let text = '';
178
+ let error = null;
179
+ try {
180
+ const response = await (context.fetch ?? fetch)(String(s['url']), {
181
+ method: 'POST',
182
+ headers: {
183
+ 'content-type': 'application/json',
184
+ 'user-agent': 'Shutro-ERP-Webhooks/1',
185
+ 'x-shutro-event': String(d['event']),
186
+ 'x-shutro-delivery': String(payload.id ?? delivery),
187
+ 'x-shutro-signature': signature(String(s['secret']), body, Math.floor(started / 1000)),
188
+ },
189
+ body,
190
+ signal: AbortSignal.timeout(10_000),
191
+ redirect: 'manual',
192
+ });
193
+ status = response.status;
194
+ text = (await response.text().catch(() => '')).slice(0, 2048);
195
+ if (!response.ok)
196
+ error = `The endpoint answered ${response.status}.`;
197
+ }
198
+ catch (e) {
199
+ error = e instanceof Error ? e.message : String(e);
200
+ }
201
+ const outcome = {
202
+ attempts,
203
+ responseStatus: status,
204
+ responseBody: text,
205
+ durationMs: Date.now() - started,
206
+ error,
207
+ };
208
+ if (error === null) {
209
+ await record({ ...outcome, state: 'done', deliveredAt: new Date() }, { failures: 0, lastDeliveryAt: new Date() });
210
+ return;
211
+ }
212
+ if (!attempt.last) {
213
+ await record({ ...outcome, state: 'pending' });
214
+ throw new Error(error); // retried with back-off
215
+ }
216
+ await record({ ...outcome, state: 'failed' }, { failures: Number(s['failures'] ?? 0) + 1 });
217
+ }
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 52fb4c0464f10e60
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 679842f099351542
2
2
  // Bindings of module `l10n_bd`: 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: 52fb4c0464f10e60
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 679842f099351542
2
2
  // Schema view of module `l10n_bd` (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;
@@ -635,6 +636,26 @@ export interface AuthCredential {
635
636
  export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
636
637
  }
637
638
 
639
+ /** `auth.policy` (owner: base). */
640
+ export interface AuthPolicy {
641
+ readonly name: 'auth.policy';
642
+ readonly fields: {
643
+ readonly id: F.Id<'auth.policy'>;
644
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
645
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
646
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
647
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
648
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
649
+ readonly twoFactor: F.Selection<'administrators' | 'everyone', { readonly required: true; readonly default: true }>;
650
+ readonly sessionDays: F.Integer;
651
+ };
652
+ readonly methods: AuthPolicyMethods;
653
+ }
654
+
655
+ /** Methods of `auth.policy`: declared ones and transitions. */
656
+ export interface AuthPolicyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.policy'> {
657
+ }
658
+
638
659
  /** `auth.session` (owner: base). */
639
660
  export interface AuthSession {
640
661
  readonly name: 'auth.session';
@@ -1129,6 +1150,13 @@ export interface ResCompany {
1129
1150
  readonly name: F.Char<{ readonly required: true }>;
1130
1151
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
1131
1152
  readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
1153
+ readonly accentColor: F.Char;
1154
+ readonly street: F.Char;
1155
+ readonly city: F.Char;
1156
+ readonly zip: F.Char;
1157
+ readonly email: F.Char;
1158
+ readonly phone: F.Char;
1159
+ readonly website: F.Char;
1132
1160
  readonly fiscalYearStart: F.Integer<{ readonly required: true; readonly default: true }>; // accounting
1133
1161
  readonly chartTemplate: F.Char; // accounting
1134
1162
  readonly country: F.Char; // accounting
@@ -1,4 +1,4 @@
1
- // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 3a48057268d9c891
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 8a0fa1636d2ec016
2
2
  // Bindings of module `l10n_in`: 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: 3a48057268d9c891
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 8a0fa1636d2ec016
2
2
  // Schema view of module `l10n_in` (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;
@@ -657,6 +658,26 @@ export interface AuthCredential {
657
658
  export interface AuthCredentialMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.credential'> {
658
659
  }
659
660
 
661
+ /** `auth.policy` (owner: base). */
662
+ export interface AuthPolicy {
663
+ readonly name: 'auth.policy';
664
+ readonly fields: {
665
+ readonly id: F.Id<'auth.policy'>;
666
+ readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
667
+ readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
668
+ readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
669
+ readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
670
+ readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
671
+ readonly twoFactor: F.Selection<'administrators' | 'everyone', { readonly required: true; readonly default: true }>;
672
+ readonly sessionDays: F.Integer;
673
+ };
674
+ readonly methods: AuthPolicyMethods;
675
+ }
676
+
677
+ /** Methods of `auth.policy`: declared ones and transitions. */
678
+ export interface AuthPolicyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.policy'> {
679
+ }
680
+
660
681
  /** `auth.session` (owner: base). */
661
682
  export interface AuthSession {
662
683
  readonly name: 'auth.session';
@@ -1187,6 +1208,13 @@ export interface ResCompany {
1187
1208
  readonly name: F.Char<{ readonly required: true }>;
1188
1209
  readonly currency: F.Many2one<'res.currency', { readonly required: true }>;
1189
1210
  readonly numberGrouping: F.Selection<'international' | 'lakh', { readonly required: true; readonly default: true }>;
1211
+ readonly accentColor: F.Char;
1212
+ readonly street: F.Char;
1213
+ readonly city: F.Char;
1214
+ readonly zip: F.Char;
1215
+ readonly email: F.Char;
1216
+ readonly phone: F.Char;
1217
+ readonly website: F.Char;
1190
1218
  readonly fiscalYearStart: F.Integer<{ readonly required: true; readonly default: true }>; // accounting
1191
1219
  readonly chartTemplate: F.Char; // accounting
1192
1220
  readonly country: F.Char; // accounting
@@ -1200,9 +1228,6 @@ export interface ResCompany {
1200
1228
  readonly accountRetainedEarnings: F.Many2one<'account.account'>; // accounting
1201
1229
  readonly gstin: F.Char; // l10n_in
1202
1230
  readonly pan: F.Char; // l10n_in
1203
- readonly street: F.Char; // l10n_in
1204
- readonly city: F.Char; // l10n_in
1205
- readonly zip: F.Char; // l10n_in
1206
1231
  readonly einvoiceEnabled: F.Bool<{ readonly required: true; readonly default: true }>; // l10n_in
1207
1232
  };
1208
1233
  readonly methods: ResCompanyMethods;
@@ -51,37 +51,6 @@ export declare const CompanyGst: import("../server/orm.js").Extension<"res.compa
51
51
  readonly hasDefault: false;
52
52
  readonly readonly: false;
53
53
  }, string>;
54
- /** Its principal place of business: the seller's address on e-invoices and e-way bills. */
55
- readonly street: import("../server/orm.js").CharField<{
56
- readonly required: false;
57
- readonly computed: false;
58
- readonly inverse: false;
59
- readonly sequence: false;
60
- readonly computeName: undefined;
61
- readonly store: true;
62
- readonly hasDefault: false;
63
- readonly readonly: false;
64
- }, string>;
65
- readonly city: import("../server/orm.js").CharField<{
66
- readonly required: false;
67
- readonly computed: false;
68
- readonly inverse: false;
69
- readonly sequence: false;
70
- readonly computeName: undefined;
71
- readonly store: true;
72
- readonly hasDefault: false;
73
- readonly readonly: false;
74
- }, string>;
75
- readonly zip: import("../server/orm.js").CharField<{
76
- readonly required: false;
77
- readonly computed: false;
78
- readonly inverse: false;
79
- readonly sequence: false;
80
- readonly computeName: undefined;
81
- readonly store: true;
82
- readonly hasDefault: false;
83
- readonly readonly: false;
84
- }, string>;
85
54
  /** Customer invoices and notes are registered with the IRP when posted (turnover above the threshold). */
86
55
  readonly einvoiceEnabled: import("../server/orm.js").ScalarField<"boolean", boolean, boolean, {
87
56
  readonly required: true;
@@ -7506,37 +7475,6 @@ export declare const l10nInShutro: import("../server/orm.js").Module<readonly [i
7506
7475
  readonly hasDefault: false;
7507
7476
  readonly readonly: false;
7508
7477
  }, string>;
7509
- /** Its principal place of business: the seller's address on e-invoices and e-way bills. */
7510
- readonly street: import("../server/orm.js").CharField<{
7511
- readonly required: false;
7512
- readonly computed: false;
7513
- readonly inverse: false;
7514
- readonly sequence: false;
7515
- readonly computeName: undefined;
7516
- readonly store: true;
7517
- readonly hasDefault: false;
7518
- readonly readonly: false;
7519
- }, string>;
7520
- readonly city: import("../server/orm.js").CharField<{
7521
- readonly required: false;
7522
- readonly computed: false;
7523
- readonly inverse: false;
7524
- readonly sequence: false;
7525
- readonly computeName: undefined;
7526
- readonly store: true;
7527
- readonly hasDefault: false;
7528
- readonly readonly: false;
7529
- }, string>;
7530
- readonly zip: import("../server/orm.js").CharField<{
7531
- readonly required: false;
7532
- readonly computed: false;
7533
- readonly inverse: false;
7534
- readonly sequence: false;
7535
- readonly computeName: undefined;
7536
- readonly store: true;
7537
- readonly hasDefault: false;
7538
- readonly readonly: false;
7539
- }, string>;
7540
7478
  /** Customer invoices and notes are registered with the IRP when posted (turnover above the threshold). */
7541
7479
  readonly einvoiceEnabled: import("../server/orm.js").ScalarField<"boolean", boolean, boolean, {
7542
7480
  readonly required: true;
@@ -101,10 +101,8 @@ export const CompanyGst = extendModel('res.company', {
101
101
  fields: {
102
102
  gstin: f.char({ size: 15, label: 'GSTIN' }),
103
103
  pan: f.char({ size: 10, label: 'PAN' }),
104
- /** Its principal place of business: the seller's address on e-invoices and e-way bills. */
105
- street: f.char({ label: 'Address' }),
106
- city: f.char({ label: 'City' }),
107
- zip: f.char({ size: 6, label: 'PIN code' }),
104
+ // Its principal place of business (`street`, `city`, `zip`: the base's company address) is the seller's
105
+ // address on e-invoices and e-way bills.
108
106
  /** Customer invoices and notes are registered with the IRP when posted (turnover above the threshold). */
109
107
  einvoiceEnabled: f.boolean({ required: true, default: false, label: 'E-invoicing' }),
110
108
  },
@@ -6,5 +6,5 @@ export { type AppChange, type AppPlan, AppPlanError, type AppReason, type AppSta
6
6
  export { idByName, maybeIdByName } from './demo.js';
7
7
  export { ModuleDefinitionError, ModuleLoadError } from './errors.js';
8
8
  export { type LoadedModules, type LoadOptions, loadModules } from './loader.js';
9
- export { type AccessLevel, type ApprovalLevelSpec, type ApprovalSpec, type AppSpec, type AutomationActionSpec, type AutomationRuleSpec, type DemoContext, type DemoUser, type DocumentSpec, defineModule, type ImportTemplateSpec, type InboundAliasSpec, type KoshModule, type LocalizationSpec, type MenuSpec, type ModuleHooks, type ModuleInput, type PortalActionSpec, type PortalDocumentSpec, type ShutroModuleShape, } from './manifest.js';
9
+ export { type AccessLevel, type ApprovalLevelSpec, type ApprovalSpec, type AppSpec, type AutomationActionSpec, type AutomationRuleSpec, type DemoContext, type DemoUser, type DocumentSpec, defineModule, type EventSpec, type ImportTemplateSpec, type InboundAliasSpec, type InboundCall, type InboundWebhookSpec, type KoshModule, type LocalizationSpec, type MenuSpec, type ModuleHooks, type ModuleInput, type PortalActionSpec, type PortalDocumentSpec, type ShutroModuleShape, } from './manifest.js';
10
10
  export { compareVersions, satisfies } from './semver.js';