shutro-erp 0.3.1 → 0.3.2

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 (48) hide show
  1. package/dist/account_portal/index.js +3 -2
  2. package/dist/accounting/index.js +4 -0
  3. package/dist/cli/init.js +2 -0
  4. package/dist/cli/launch.js +8 -5
  5. package/dist/cli/templates.js +4 -1
  6. package/dist/cli/ui.js +19 -1
  7. package/dist/integrations/inbound.d.ts +36 -0
  8. package/dist/integrations/inbound.js +171 -0
  9. package/dist/integrations/index.d.ts +18 -0
  10. package/dist/integrations/index.js +400 -0
  11. package/dist/integrations/models.d.ts +6 -0
  12. package/dist/integrations/models.js +119 -0
  13. package/dist/integrations/web/DeliveryTools.d.ts +4 -0
  14. package/dist/integrations/web/DeliveryTools.js +15 -0
  15. package/dist/integrations/web/EndpointTools.d.ts +7 -0
  16. package/dist/integrations/web/EndpointTools.js +25 -0
  17. package/dist/integrations/web/InboundTools.d.ts +4 -0
  18. package/dist/integrations/web/InboundTools.js +15 -0
  19. package/dist/integrations/web/WebhookTools.d.ts +7 -0
  20. package/dist/integrations/web/WebhookTools.js +25 -0
  21. package/dist/integrations/web/index.d.ts +16 -0
  22. package/dist/integrations/web/index.js +27 -0
  23. package/dist/integrations/web/messages.d.ts +2 -0
  24. package/dist/integrations/web/messages.js +53 -0
  25. package/dist/integrations/webhooks.d.ts +63 -0
  26. package/dist/integrations/webhooks.js +217 -0
  27. package/dist/modules-sdk/index.d.ts +1 -1
  28. package/dist/modules-sdk/manifest.d.ts +48 -0
  29. package/dist/payment/.kosh/bindings.d.ts +22 -0
  30. package/dist/payment/.kosh/bindings.js +15 -0
  31. package/dist/payment/.kosh/schema.d.ts +1553 -0
  32. package/dist/payment/index.d.ts +34 -0
  33. package/dist/payment/index.js +375 -0
  34. package/dist/payment/module.d.ts +6853 -0
  35. package/dist/payment/module.js +74 -0
  36. package/dist/payment/providers.d.ts +64 -0
  37. package/dist/payment/providers.js +77 -0
  38. package/dist/payment/settle.d.ts +11 -0
  39. package/dist/payment/settle.js +59 -0
  40. package/dist/payment/sslcommerz.d.ts +12 -0
  41. package/dist/payment/sslcommerz.js +185 -0
  42. package/dist/portal/routes-documents.js +5 -0
  43. package/dist/sales/index.js +2 -0
  44. package/dist/server/api/rest.d.ts +6 -1
  45. package/dist/server/api/rest.js +11 -0
  46. package/dist/server/index.d.ts +1 -1
  47. package/dist/server/index.js +1 -1
  48. package/package.json +13 -1
@@ -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
+ }
@@ -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';
@@ -97,6 +97,43 @@ export interface AutomationRuleSpec {
97
97
  /** Installed switched on (default) or off. */
98
98
  readonly enabled?: boolean;
99
99
  }
100
+ /**
101
+ * An event a module emits through Shutro's outbox (`env.emit(topic, { id, … })`), which webhooks and integrations
102
+ * may subscribe to (RFC 0017 §5.3): the topic, the model whose record `id` names, and what it means.
103
+ */
104
+ export interface EventSpec {
105
+ readonly topic: string;
106
+ readonly model: string;
107
+ readonly label: string;
108
+ }
109
+ /** A call received by an inbound webhook (RFC 0017 §5.4): the raw body (what signatures cover) and its JSON. */
110
+ export interface InboundCall {
111
+ readonly headers: Readonly<Record<string, string | undefined>>;
112
+ readonly query: Readonly<Record<string, string | undefined>>;
113
+ /** The body as received. */
114
+ readonly body: string;
115
+ /** The body parsed as JSON, or null. */
116
+ readonly json: unknown;
117
+ }
118
+ /**
119
+ * An endpoint a module receives calls on from outside (RFC 0017 §5.4): a payment provider's callback, a shipping
120
+ * status. Companies switch it on in Integrations › Inbound webhooks (a URL and a secret each).
121
+ */
122
+ export interface InboundWebhookSpec {
123
+ /** Unique within the module (`ipn`); the endpoint is `<module>.<key>`. */
124
+ readonly key: string;
125
+ readonly label: string;
126
+ /**
127
+ * Whether the call is genuine — the provider's signature, or an HMAC with the endpoint's secret — and its event
128
+ * id, by which repeated calls are recognised: the id, or null to refuse the call.
129
+ */
130
+ readonly verify: (call: InboundCall, secret: string) => string | null | Promise<string | null>;
131
+ /**
132
+ * Handles a verified call, once per event id, in a job (retried on failure): `env` is a system environment in
133
+ * the endpoint's company. Returns a short note for the log (a string; anything else is not kept).
134
+ */
135
+ readonly handle: (env: unknown, call: InboundCall) => Promise<unknown>;
136
+ }
100
137
  /** A default document template of a module. */
101
138
  export interface DocumentSpec {
102
139
  /** Unique within the module (`purchase_order`); stored as `module.key`. */
@@ -175,6 +212,13 @@ export interface PortalDocumentSpec {
175
212
  * email with a link, once. The model's writes must reach the portal: list it in the module's `changeEvents`.
176
213
  */
177
214
  readonly announce?: Readonly<Record<string, unknown>>;
215
+ /**
216
+ * The customer may pay it online (RFC 0017 §5.5) while it is in this domain (`{ paymentState: 'not_paid' }`):
217
+ * the document's page offers Pay now when a payment module and a provider are there.
218
+ */
219
+ readonly payment?: {
220
+ readonly when: Readonly<Record<string, unknown>>;
221
+ };
178
222
  }
179
223
  /**
180
224
  * A country localization (RFC 0016 §5.2): what installing it sets up for a company, once — its chart of accounts,
@@ -367,6 +411,10 @@ export interface ModuleInput {
367
411
  readonly externalGroups?: readonly string[];
368
412
  /** The country this module localizes (RFC 0016): set up for each company when the module is installed. */
369
413
  readonly localization?: LocalizationSpec;
414
+ /** Events the module emits that webhooks may subscribe to (RFC 0017 §5.3). */
415
+ readonly events?: readonly EventSpec[];
416
+ /** Endpoints the module receives calls on from outside (RFC 0017 §5.4). */
417
+ readonly inboundWebhooks?: readonly InboundWebhookSpec[];
370
418
  /** What the module shows in the customer and vendor portal (RFC 0015), when `portal` is installed. */
371
419
  readonly portal?: {
372
420
  readonly documents: readonly PortalDocumentSpec[];
@@ -0,0 +1,22 @@
1
+ export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
2
+ import { type DefinitionHelpers } from '../../server/orm.js';
3
+ import type { DefaultLoaded, Env as CoreEnv, RecordSet as CoreRecordSet, Shutro as CoreShutro } from '../../server/orm.js';
4
+ import type { SchemaView } from './schema.js';
5
+ export type { SchemaView };
6
+ /** Every model visible in this view. */
7
+ export type ModelName = keyof SchemaView & string;
8
+ /** `Env` typed to this view. */
9
+ export type Env = CoreEnv<SchemaView>;
10
+ /** `Shutro` typed to this view. */
11
+ export type Shutro = CoreShutro<SchemaView>;
12
+ /** A recordset of `M`, with the default loaded shape unless `L` is given. */
13
+ export type Records<M extends ModelName, L = DefaultLoaded<SchemaView, M>> = CoreRecordSet<SchemaView, M, L>;
14
+ export declare const compute: DefinitionHelpers<SchemaView>['compute'];
15
+ export declare const check: DefinitionHelpers<SchemaView>['check'];
16
+ export declare const onchange: DefinitionHelpers<SchemaView>['onchange'];
17
+ export declare const records: DefinitionHelpers<SchemaView>['records'];
18
+ export declare const method: DefinitionHelpers<SchemaView>['method'];
19
+ /** Module data (RFC 0017): records, overrides of dependencies' records, and references, typed like creates. */
20
+ export declare const record: DefinitionHelpers<SchemaView>['record'];
21
+ export declare const override: DefinitionHelpers<SchemaView>['override'];
22
+ export declare const ref: DefinitionHelpers<SchemaView>['ref'];
@@ -0,0 +1,15 @@
1
+ // GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 51b637962c6e5497
2
+ // Bindings of module `payment`: import the definition API from here to get types scoped to what it can see.
3
+ export { defineModel, defineModule, extendModel, f, replace } from '../../server/orm.js';
4
+ import { definitions } from '../../server/orm.js';
5
+ /** Definition callbacks typed against this module's view: no casts in computes, checks, onchange, methods. */
6
+ const helpers = definitions();
7
+ export const compute = helpers.compute;
8
+ export const check = helpers.check;
9
+ export const onchange = helpers.onchange;
10
+ export const records = helpers.records;
11
+ export const method = helpers.method;
12
+ /** Module data (RFC 0017): records, overrides of dependencies' records, and references, typed like creates. */
13
+ export const record = helpers.record;
14
+ export const override = helpers.override;
15
+ export const ref = helpers.ref;