shutro-erp 0.3.0 → 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.
- package/README.md +58 -58
- package/dist/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +31 -1
- package/dist/account_portal/index.js +3 -2
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +31 -1
- package/dist/accounting/index.js +4 -0
- package/dist/assistant/routes.js +1 -1
- package/dist/automation/engine.js +1 -1
- package/dist/automation/web/messages.js +2 -2
- package/dist/cli/apps.d.ts +1 -1
- package/dist/cli/apps.js +14 -14
- package/dist/cli/bin.js +3 -0
- package/dist/cli/index.js +11 -11
- package/dist/cli/init.d.ts +1 -1
- package/dist/cli/init.js +60 -36
- package/dist/cli/launch.js +10 -7
- package/dist/cli/project.d.ts +7 -7
- package/dist/cli/project.js +25 -25
- package/dist/cli/prompt.js +2 -2
- package/dist/cli/templates.d.ts +3 -3
- package/dist/cli/templates.js +33 -28
- package/dist/cli/ui.d.ts +1 -8
- package/dist/cli/ui.js +36 -12
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +31 -1
- package/dist/integrations/inbound.d.ts +36 -0
- package/dist/integrations/inbound.js +171 -0
- package/dist/integrations/index.d.ts +18 -0
- package/dist/integrations/index.js +400 -0
- package/dist/integrations/models.d.ts +6 -0
- package/dist/integrations/models.js +119 -0
- package/dist/integrations/web/DeliveryTools.d.ts +4 -0
- package/dist/integrations/web/DeliveryTools.js +15 -0
- package/dist/integrations/web/EndpointTools.d.ts +7 -0
- package/dist/integrations/web/EndpointTools.js +25 -0
- package/dist/integrations/web/InboundTools.d.ts +4 -0
- package/dist/integrations/web/InboundTools.js +15 -0
- package/dist/integrations/web/WebhookTools.d.ts +7 -0
- package/dist/integrations/web/WebhookTools.js +25 -0
- package/dist/integrations/web/index.d.ts +16 -0
- package/dist/integrations/web/index.js +27 -0
- package/dist/integrations/web/messages.d.ts +2 -0
- package/dist/integrations/web/messages.js +53 -0
- package/dist/integrations/webhooks.d.ts +63 -0
- package/dist/integrations/webhooks.js +217 -0
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +31 -1
- package/dist/l10n_in/einvoice.js +1 -1
- package/dist/l10n_in/web/messages.js +2 -2
- package/dist/mail/index.js +2 -2
- package/dist/modules-sdk/index.d.ts +1 -1
- package/dist/modules-sdk/loader.js +2 -2
- package/dist/modules-sdk/manifest.d.ts +48 -0
- package/dist/notify/messages.js +3 -3
- package/dist/notify/routes.js +1 -1
- package/dist/notify/web/messages.js +1 -1
- package/dist/payment/.kosh/bindings.d.ts +22 -0
- package/dist/payment/.kosh/bindings.js +15 -0
- package/dist/payment/.kosh/schema.d.ts +1553 -0
- package/dist/payment/index.d.ts +34 -0
- package/dist/payment/index.js +375 -0
- package/dist/payment/module.d.ts +6853 -0
- package/dist/payment/module.js +74 -0
- package/dist/payment/providers.d.ts +64 -0
- package/dist/payment/providers.js +77 -0
- package/dist/payment/settle.d.ts +11 -0
- package/dist/payment/settle.js +59 -0
- package/dist/payment/sslcommerz.d.ts +12 -0
- package/dist/payment/sslcommerz.js +185 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +31 -1
- package/dist/portal/routes-documents.js +5 -0
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +31 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +31 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +31 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +31 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +31 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +31 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +31 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +31 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +31 -1
- package/dist/sales/index.js +2 -0
- package/dist/server/api/client.js +1 -1
- package/dist/server/api/openapi.js +17 -1
- package/dist/server/api/rest.d.ts +6 -1
- package/dist/server/api/rest.js +11 -0
- package/dist/server/apps.js +5 -5
- package/dist/server/auth/api-keys.d.ts +13 -0
- package/dist/server/auth/api-keys.js +65 -0
- package/dist/server/auth/base.js +24 -0
- package/dist/server/auth/crypto.js +1 -1
- package/dist/server/auth/scopes.d.ts +12 -0
- package/dist/server/auth/scopes.js +51 -0
- package/dist/server/auth/service.d.ts +57 -1
- package/dist/server/auth/service.js +229 -5
- package/dist/server/demo.d.ts +1 -1
- package/dist/server/demo.js +1 -1
- package/dist/server/env-names.d.ts +2 -0
- package/dist/server/env-names.js +16 -0
- package/dist/server/host.js +5 -1
- package/dist/server/identity.d.ts +7 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/registry.js +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.js +45 -5
- package/dist/settings/routes.js +1 -1
- package/dist/settings/web/ApiKeysPage.d.ts +8 -0
- package/dist/settings/web/ApiKeysPage.js +70 -0
- package/dist/settings/web/index.d.ts +1 -0
- package/dist/settings/web/index.js +4 -0
- package/dist/settings/web/messages.js +62 -0
- package/dist/storage/routes.js +1 -1
- package/dist/studio/build.js +1 -1
- package/dist/studio/generate.js +4 -4
- package/dist/studio/module.js +5 -5
- package/dist/studio/web/messages.js +2 -2
- package/dist/ui/app.js +26 -3
- package/dist/ui/i18n/messages.js +2 -2
- package/dist/ui/pwa/worker.js +2 -2
- package/dist/ui/registry.d.ts +15 -0
- package/dist/ui/registry.js +20 -0
- package/package.json +15 -3
package/dist/cli/templates.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The starter project `
|
|
2
|
+
* The starter project `shutro-erp init` writes: an API server and the back-office app, both importing `shutro-erp`.
|
|
3
3
|
* Server files run on Node.js directly (type stripping); the back-office is a Vite app proxying `/api` to the server.
|
|
4
4
|
*/
|
|
5
5
|
export type AppName = 'permissions' | 'reporting' | 'messaging' | 'contacts' | 'product' | 'sales' | 'purchase' | 'accounting' | 'sale_account' | 'purchase_account' | 'project' | 'sale_project' | 'l10n_bd' | 'l10n_in';
|
|
@@ -16,7 +16,7 @@ export interface AppSpec {
|
|
|
16
16
|
/** A localization (RFC 0016): installed, with its bridged apps, for a company in this country (ISO 3166). */
|
|
17
17
|
readonly country?: string;
|
|
18
18
|
}
|
|
19
|
-
/** The apps `
|
|
19
|
+
/** The apps `shutro-erp init` can install (base and Settings are always there), in the order the installer lists them. */
|
|
20
20
|
export declare const APPS: Readonly<Record<AppName, AppSpec>>;
|
|
21
21
|
export interface StarterOptions {
|
|
22
22
|
readonly name: string;
|
|
@@ -27,7 +27,7 @@ export interface StarterOptions {
|
|
|
27
27
|
readonly koshVersion: string;
|
|
28
28
|
/** The database name of the Docker PostgreSQL service (compose.yaml). */
|
|
29
29
|
readonly dockerDb?: string;
|
|
30
|
-
/** Attachments in a folder (
|
|
30
|
+
/** Attachments in a folder (SHUTRO_ERP_STORAGE_DIR) rather than S3. */
|
|
31
31
|
readonly fileStorage?: boolean;
|
|
32
32
|
}
|
|
33
33
|
/** The project's package.json (merged into an existing one: missing scripts and dependencies are added). */
|
package/dist/cli/templates.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The starter project `
|
|
2
|
+
* The starter project `shutro-erp init` writes: an API server and the back-office app, both importing `shutro-erp`.
|
|
3
3
|
* Server files run on Node.js directly (type stripping); the back-office is a Vite app proxying `/api` to the server.
|
|
4
4
|
*/
|
|
5
|
-
/** The apps `
|
|
5
|
+
/** The apps `shutro-erp init` can install (base and Settings are always there), in the order the installer lists them. */
|
|
6
6
|
export const APPS = {
|
|
7
7
|
permissions: {
|
|
8
8
|
label: 'Roles, field security and approvals',
|
|
@@ -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
|
};
|
|
@@ -105,8 +106,8 @@ export function packageJson(options, existing) {
|
|
|
105
106
|
start: 'node --env-file=.env src/server.ts',
|
|
106
107
|
web: 'vite',
|
|
107
108
|
'build:web': 'vite build',
|
|
108
|
-
migrate: '
|
|
109
|
-
seed: '
|
|
109
|
+
migrate: 'shutro-erp migrate',
|
|
110
|
+
seed: 'shutro-erp seed',
|
|
110
111
|
'docker:up': 'docker compose up -d --build',
|
|
111
112
|
}),
|
|
112
113
|
engines: { node: '>=24', ...(base['engines'] ?? {}) },
|
|
@@ -130,12 +131,13 @@ export function modulesTs(_options) {
|
|
|
130
131
|
const imports = apps.map((a) => `import { ${APPS[a].server} } from 'shutro-erp/${a}';`);
|
|
131
132
|
return `/**
|
|
132
133
|
* The apps this installation can install (the catalogue). The database records which are installed: Settings → Apps
|
|
133
|
-
* in the back-office, or \`npx
|
|
134
|
+
* in the back-office, or \`npx shutro-erp apps\`, installs, disables and enables them. \`base\` (users, companies, sign-in)
|
|
134
135
|
* and Settings are always installed. Add your own modules here (\`defineModule\` from \`shutro-erp/modules-sdk\`),
|
|
135
136
|
* then install them from Settings → Apps.
|
|
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,12 +147,12 @@ 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() {
|
|
152
154
|
return `/**
|
|
153
|
-
* The
|
|
155
|
+
* The Shutro ERP API server: \`npm start\` (reads .env). It serves the installed apps of src/modules.ts, and switches to
|
|
154
156
|
* the new set without a restart when apps are installed or disabled (here or on another server).
|
|
155
157
|
*/
|
|
156
158
|
import { createAppHost, startAppHost } from 'shutro-erp';
|
|
@@ -162,11 +164,11 @@ const host = await createAppHost({
|
|
|
162
164
|
catalogue: modules,
|
|
163
165
|
migrationsDir: 'migrations',
|
|
164
166
|
...(env['VALKEY_URL'] ? { valkeyUrl: env['VALKEY_URL'] } : {}),
|
|
165
|
-
// Secure cookies need HTTPS: on by default in production (
|
|
166
|
-
auth: { secureCookies: (env['KOSH_SECURE_COOKIES'] ?? String(env['NODE_ENV'] === 'production')) === 'true' },
|
|
167
|
+
// Secure cookies need HTTPS: on by default in production (SHUTRO_ERP_SECURE_COOKIES=false for plain HTTP).
|
|
168
|
+
auth: { secureCookies: (env['SHUTRO_ERP_SECURE_COOKIES'] ?? env['KOSH_SECURE_COOKIES'] ?? String(env['NODE_ENV'] === 'production')) === 'true' },
|
|
167
169
|
});
|
|
168
170
|
const address = await startAppHost(host, { port: Number(env['PORT'] ?? 3000), host: env['HOST'] ?? '127.0.0.1' });
|
|
169
|
-
process.stdout.write(\`
|
|
171
|
+
process.stdout.write(\`Shutro ERP API on \${address}\\n\`);
|
|
170
172
|
`;
|
|
171
173
|
}
|
|
172
174
|
export function viteConfig(options) {
|
|
@@ -176,7 +178,7 @@ import react from '@vitejs/plugin-react';
|
|
|
176
178
|
import { koshPwa } from 'shutro-erp/ui/vite';
|
|
177
179
|
import { defineConfig } from 'vite';
|
|
178
180
|
|
|
179
|
-
const api = process.env['KOSH_API'] ?? 'http://localhost:${options.port}';
|
|
181
|
+
const api = process.env['SHUTRO_ERP_API'] ?? process.env['KOSH_API'] ?? 'http://localhost:${options.port}';
|
|
180
182
|
|
|
181
183
|
export default defineConfig({
|
|
182
184
|
// koshPwa: the service worker (the app opens offline and gets web push)
|
|
@@ -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' },
|
|
@@ -263,7 +266,7 @@ export function manifest(options) {
|
|
|
263
266
|
}
|
|
264
267
|
/** The app icon (replace it with the company's). */
|
|
265
268
|
export function icon() {
|
|
266
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="
|
|
269
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Shutro ERP">
|
|
267
270
|
<rect width="512" height="512" rx="112" fill="#1d4ed8"/>
|
|
268
271
|
<path d="M156 128h56v112l108-112h72L270 258l128 126h-74L220 282v102h-64z" fill="#fff"/>
|
|
269
272
|
</svg>
|
|
@@ -273,7 +276,7 @@ export function indexCss() {
|
|
|
273
276
|
return `@import "tailwindcss";
|
|
274
277
|
@import "shutro-erp/ui/styles.css";
|
|
275
278
|
|
|
276
|
-
/* Tailwind scans
|
|
279
|
+
/* Tailwind scans Shutro ERP's components for the classes they use. */
|
|
277
280
|
@source "../../node_modules/shutro-erp/dist";
|
|
278
281
|
`;
|
|
279
282
|
}
|
|
@@ -286,7 +289,7 @@ var/
|
|
|
286
289
|
}
|
|
287
290
|
/** Three images from one build: the API (migrates, then serves) and nginx with the back-office. */
|
|
288
291
|
export function dockerfile() {
|
|
289
|
-
return `#
|
|
292
|
+
return `# Shutro ERP images (shutro-erp init). Built by compose.yaml: docker compose up -d --build
|
|
290
293
|
FROM node:24-alpine AS build
|
|
291
294
|
WORKDIR /app
|
|
292
295
|
COPY package.json package-lock.json* ./
|
|
@@ -306,7 +309,7 @@ COPY --from=build /app/migrations ./migrations
|
|
|
306
309
|
RUN mkdir -p /app/var/attachments && chown -R node:node /app/var
|
|
307
310
|
USER node
|
|
308
311
|
EXPOSE 3000
|
|
309
|
-
CMD ["sh", "-c", "npx
|
|
312
|
+
CMD ["sh", "-c", "npx shutro-erp migrate && node src/server.ts"]
|
|
310
313
|
|
|
311
314
|
# The back-office: static files, /api proxied to the API.
|
|
312
315
|
FROM nginx:1.27-alpine AS web
|
|
@@ -315,7 +318,7 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
|
315
318
|
`;
|
|
316
319
|
}
|
|
317
320
|
export function nginxConf() {
|
|
318
|
-
return `# The back-office (
|
|
321
|
+
return `# The back-office (shutro-erp init): the built app, /api proxied to the API service.
|
|
319
322
|
server {
|
|
320
323
|
listen 80;
|
|
321
324
|
root /usr/share/nginx/html;
|
|
@@ -346,9 +349,9 @@ export function composeYaml(options) {
|
|
|
346
349
|
const db = options.dockerDb ?? options.name.replace(/-/g, '_');
|
|
347
350
|
/** A compose variable: `${NAME}` or `${NAME:-default}` (plain strings: nothing to escape). */
|
|
348
351
|
const v = (name, fallback) => ['$', '{', name, fallback === undefined ? '' : fallback, '}'].join('');
|
|
349
|
-
const user = v('POSTGRES_USER', ':-
|
|
352
|
+
const user = v('POSTGRES_USER', ':-shutro');
|
|
350
353
|
const lines = [
|
|
351
|
-
'#
|
|
354
|
+
'# Shutro ERP in Docker (shutro-erp init).',
|
|
352
355
|
'# docker compose up -d db only PostgreSQL (the API runs with npm start)',
|
|
353
356
|
'# docker compose up -d --build everything: the back-office on http://localhost:8080',
|
|
354
357
|
`name: ${options.name}`,
|
|
@@ -367,7 +370,7 @@ export function composeYaml(options) {
|
|
|
367
370
|
' - db:/var/lib/postgresql',
|
|
368
371
|
' healthcheck:',
|
|
369
372
|
// $$ is a literal $ for compose: the shell in the container expands the variable.
|
|
370
|
-
` test: ["CMD-SHELL", "pg_isready -U $${v('POSTGRES_USER', ':-
|
|
373
|
+
` test: ["CMD-SHELL", "pg_isready -U $${v('POSTGRES_USER', ':-shutro')}"]`,
|
|
371
374
|
' interval: 5s',
|
|
372
375
|
' retries: 30',
|
|
373
376
|
'',
|
|
@@ -379,9 +382,11 @@ export function composeYaml(options) {
|
|
|
379
382
|
' env_file: .env',
|
|
380
383
|
' environment:',
|
|
381
384
|
` DATABASE_URL: ${v('DOCKER_DATABASE_URL', `:-postgres://${user}:${v('POSTGRES_PASSWORD')}@db:5432/${v('POSTGRES_DB', `:-${db}`)}`)}`,
|
|
382
|
-
...(options.fileStorage === false
|
|
385
|
+
...(options.fileStorage === false
|
|
386
|
+
? []
|
|
387
|
+
: [' SHUTRO_ERP_STORAGE_DIR: /app/var/attachments']),
|
|
383
388
|
' # Plain HTTP on localhost; set "true" when the site is served over HTTPS.',
|
|
384
|
-
'
|
|
389
|
+
' SHUTRO_ERP_SECURE_COOKIES: "false"',
|
|
385
390
|
' depends_on:',
|
|
386
391
|
' db:',
|
|
387
392
|
' condition: service_healthy',
|
|
@@ -431,19 +436,19 @@ export function tsconfig() {
|
|
|
431
436
|
export function readme(options) {
|
|
432
437
|
return `# ${options.name}
|
|
433
438
|
|
|
434
|
-
A [
|
|
439
|
+
A [Shutro ERP](https://www.npmjs.com/package/shutro-erp) installation, created with \`shutro-erp init\`.
|
|
435
440
|
|
|
436
441
|
\`\`\`sh
|
|
437
442
|
npm install
|
|
438
443
|
npm start # the API on http://localhost:${options.port}
|
|
439
444
|
npm run web # the back-office on http://localhost:${options.webPort ?? 5173} (sign in as the administrator)
|
|
440
|
-
npx
|
|
441
|
-
npx
|
|
445
|
+
npx shutro-erp migrate # after changing modules: plan and apply database migrations, load seed data
|
|
446
|
+
npx shutro-erp seed # load seed data again
|
|
442
447
|
# Settings → Apps in the back-office installs, enables and disables apps (or, in the terminal:)
|
|
443
|
-
npx
|
|
444
|
-
npx
|
|
445
|
-
npx
|
|
446
|
-
npx
|
|
448
|
+
npx shutro-erp apps # the apps: installed, disabled, available
|
|
449
|
+
npx shutro-erp apps install <name> # install an app (and what it needs) in one transaction
|
|
450
|
+
npx shutro-erp apps disable <name> # switch an app off; its data stays (npx shutro-erp apps enable <name>)
|
|
451
|
+
npx shutro-erp module create <name> # a new module in src/modules/<name>/
|
|
447
452
|
docker compose up -d --build # everything in Docker: http://localhost:8080
|
|
448
453
|
\`\`\`
|
|
449
454
|
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The installer's look in the terminal, without dependencies: a logo, numbered steps, spinners for long work,
|
|
3
|
-
* status lines (✔ ▲ ✖) and a summary card. On a Unicode terminal everything hangs on one rail down the left (│),
|
|
4
|
-
* the questions of the interactive prompter included (`tui.ts`). Colour, Unicode and animation are detected from
|
|
5
|
-
* the terminal (NO_COLOR, FORCE_COLOR, KOSH_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not
|
|
6
|
-
* a terminal (tests, logs, CI) gets plain lines with the same words and no escape codes.
|
|
7
|
-
*/
|
|
8
1
|
/** What the terminal can show. */
|
|
9
2
|
export interface Terminal {
|
|
10
3
|
readonly color: boolean;
|
|
@@ -23,7 +16,7 @@ type Env = Readonly<Record<string, string | undefined>>;
|
|
|
23
16
|
export declare function detectTerminal(stream: {
|
|
24
17
|
readonly isTTY?: boolean;
|
|
25
18
|
readonly columns?: number;
|
|
26
|
-
}, env?: Env, platform?: string): Terminal;
|
|
19
|
+
}, env?: Env, platform?: string, release?: string): Terminal;
|
|
27
20
|
export declare const ESC = "\u001B[";
|
|
28
21
|
/** The colours and weights the installer uses. */
|
|
29
22
|
export type Style = 'bold' | 'dim' | 'inverse' | 'underline' | 'green' | 'yellow' | 'red' | 'cyan' | 'magenta' | 'gray';
|
package/dist/cli/ui.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* The installer's look in the terminal, without dependencies: a logo, numbered steps, spinners for long work,
|
|
3
3
|
* status lines (✔ ▲ ✖) and a summary card. On a Unicode terminal everything hangs on one rail down the left (│),
|
|
4
4
|
* the questions of the interactive prompter included (`tui.ts`). Colour, Unicode and animation are detected from
|
|
5
|
-
* the terminal (NO_COLOR, FORCE_COLOR,
|
|
5
|
+
* the terminal (NO_COLOR, FORCE_COLOR, SHUTRO_ERP_ASCII=1, TERM=dumb, Windows consoles without Unicode); output that is not
|
|
6
6
|
* a terminal (tests, logs, CI) gets plain lines with the same words and no escape codes.
|
|
7
7
|
*/
|
|
8
|
+
import { release as osRelease } from 'node:os';
|
|
8
9
|
/** Plain output: logs, tests, pipes. */
|
|
9
10
|
export const PLAIN_TERMINAL = Object.freeze({
|
|
10
11
|
color: false,
|
|
@@ -14,7 +15,7 @@ export const PLAIN_TERMINAL = Object.freeze({
|
|
|
14
15
|
columns: 80,
|
|
15
16
|
});
|
|
16
17
|
/** Detects what `stream` (process.stdout) can show. */
|
|
17
|
-
export function detectTerminal(stream, env = process.env, platform = process.platform) {
|
|
18
|
+
export function detectTerminal(stream, env = process.env, platform = process.platform, release = osRelease()) {
|
|
18
19
|
const tty = stream.isTTY === true && env['TERM'] !== 'dumb' && env['CI'] === undefined;
|
|
19
20
|
const forced = env['FORCE_COLOR'] !== undefined && env['FORCE_COLOR'] !== '0';
|
|
20
21
|
const color = env['NO_COLOR'] === undefined && (forced || tty);
|
|
@@ -24,11 +25,16 @@ export function detectTerminal(stream, env = process.env, platform = process.pla
|
|
|
24
25
|
env['ConEmuTask'] === '{cmd::Cmder}' ||
|
|
25
26
|
env['TERM'] === 'xterm-256color' ||
|
|
26
27
|
env['TERM'] === 'alacritty';
|
|
27
|
-
|
|
28
|
+
// Windows 10 and later: the console host shows box drawing and colours too (UTF-8, virtual terminal); pictographs
|
|
29
|
+
// only in the modern terminals. Older Windows keeps ASCII.
|
|
30
|
+
const windows10 = Number(release.split('.')[0]) >= 10;
|
|
31
|
+
const unicode = env['KOSH_ASCII'] !== '1' &&
|
|
32
|
+
env['SHUTRO_ERP_ASCII'] !== '1' &&
|
|
33
|
+
(platform === 'win32' ? modernWindows || windows10 : env['TERM'] !== 'linux');
|
|
28
34
|
return {
|
|
29
35
|
color,
|
|
30
36
|
unicode,
|
|
31
|
-
emoji: unicode && tty,
|
|
37
|
+
emoji: unicode && tty && (platform !== 'win32' || modernWindows),
|
|
32
38
|
animate: tty,
|
|
33
39
|
columns: Math.max(40, Math.min(stream.columns ?? 80, 120)),
|
|
34
40
|
};
|
|
@@ -92,12 +98,12 @@ export const ICONS = {
|
|
|
92
98
|
rocket: ['🚀', '◆'],
|
|
93
99
|
};
|
|
94
100
|
const LOGO = [
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
'██║
|
|
100
|
-
'
|
|
101
|
+
'███████╗██╗ ██╗██╗ ██╗████████╗██████╗ ██████╗',
|
|
102
|
+
'██╔════╝██║ ██║██║ ██║╚══██╔══╝██╔══██╗██╔═══██╗',
|
|
103
|
+
'███████╗███████║██║ ██║ ██║ ██████╔╝██║ ██║',
|
|
104
|
+
'╚════██║██╔══██║██║ ██║ ██║ ██╔══██╗██║ ██║',
|
|
105
|
+
'███████║██║ ██║╚██████╔╝ ██║ ██║ ██║╚██████╔╝',
|
|
106
|
+
'╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝',
|
|
101
107
|
];
|
|
102
108
|
/** Cyan to violet, one 256-colour step per logo line. */
|
|
103
109
|
const GRADIENT = [51, 45, 39, 33, 63, 99];
|
|
@@ -141,7 +147,7 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
141
147
|
icon,
|
|
142
148
|
async banner(subtitle) {
|
|
143
149
|
if (!terminal.unicode) {
|
|
144
|
-
write(`\n ${s('bold', '
|
|
150
|
+
write(`\n ${s('bold', 'S H U T R O E R P')} ${subtitle}\n`);
|
|
145
151
|
return;
|
|
146
152
|
}
|
|
147
153
|
write('\n');
|
|
@@ -245,7 +251,25 @@ export function createUi(out, terminal = PLAIN_TERMINAL, options = {}) {
|
|
|
245
251
|
},
|
|
246
252
|
card(title, rows, tone = 'green') {
|
|
247
253
|
const labelWidth = Math.max(0, ...rows.map(([l]) => width(l)));
|
|
248
|
-
|
|
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}`));
|
|
249
273
|
const inner = Math.max(width(title), ...body.map(width)) + 2;
|
|
250
274
|
const [tl, tr, bl, br, h, v] = terminal.unicode
|
|
251
275
|
? ['╭', '╮', '╰', '╯', '─', '│']
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
|
|
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,10 +1,11 @@
|
|
|
1
|
-
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash:
|
|
1
|
+
// GENERATED by `pnpm generate:models` (Shutro’s generator). Do not edit. definitions-hash: 2271aaa68e1a18a5
|
|
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
|
|
|
5
5
|
/** Model name → model type (RFC 0002 §7.7). */
|
|
6
6
|
export interface SchemaView {
|
|
7
7
|
readonly 'audit.event': AuditEvent;
|
|
8
|
+
readonly 'auth.api_key': AuthApiKey;
|
|
8
9
|
readonly 'auth.credential': AuthCredential;
|
|
9
10
|
readonly 'auth.session': AuthSession;
|
|
10
11
|
readonly 'auth.user_grant': AuthUserGrant;
|
|
@@ -42,6 +43,34 @@ export interface AuditEvent {
|
|
|
42
43
|
export interface AuditEventMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'audit.event'> {
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** `auth.api_key` (owner: base). */
|
|
47
|
+
export interface AuthApiKey {
|
|
48
|
+
readonly name: 'auth.api_key';
|
|
49
|
+
readonly fields: {
|
|
50
|
+
readonly id: F.Id<'auth.api_key'>;
|
|
51
|
+
readonly createdAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
52
|
+
readonly updatedAt: F.Datetime<{ readonly required: true; readonly readonly: true }>;
|
|
53
|
+
readonly createdBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
54
|
+
readonly updatedBy: F.Many2one<'res.users', { readonly readonly: true }>;
|
|
55
|
+
readonly displayName: F.Char<{ readonly required: true; readonly readonly: true; readonly compute: true }>;
|
|
56
|
+
readonly name: F.Char<{ readonly required: true }>;
|
|
57
|
+
readonly user: F.Many2one<'res.users', { readonly required: true }>;
|
|
58
|
+
readonly tokenHash: F.Char<{ readonly required: true }>;
|
|
59
|
+
readonly prefix: F.Char<{ readonly required: true }>;
|
|
60
|
+
readonly scopes: F.Text<{ readonly required: true }>;
|
|
61
|
+
readonly expiresAt: F.Datetime<{ readonly required: true }>;
|
|
62
|
+
readonly lastUsedAt: F.Datetime;
|
|
63
|
+
readonly lastUsedIp: F.Char;
|
|
64
|
+
readonly revoked: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
65
|
+
readonly issuedBy: F.Many2one<'res.users'>;
|
|
66
|
+
};
|
|
67
|
+
readonly methods: AuthApiKeyMethods;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Methods of `auth.api_key`: declared ones and transitions. */
|
|
71
|
+
export interface AuthApiKeyMethods extends F.Methods<typeof import('../../server/index.js').baseShutro, 'auth.api_key'> {
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
/** `auth.credential` (owner: base). */
|
|
46
75
|
export interface AuthCredential {
|
|
47
76
|
readonly name: 'auth.credential';
|
|
@@ -260,6 +289,7 @@ export interface ResUsers {
|
|
|
260
289
|
readonly companies: F.Many2many<'res.company', { readonly required: true }>;
|
|
261
290
|
readonly lang: F.Char<{ readonly default: true }>;
|
|
262
291
|
readonly tz: F.Char<{ readonly default: true }>;
|
|
292
|
+
readonly isService: F.Bool<{ readonly required: true; readonly default: true }>;
|
|
263
293
|
};
|
|
264
294
|
readonly methods: ResUsersMethods;
|
|
265
295
|
}
|
|
@@ -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;
|