@k2b/cloud 0.40.0 → 0.42.0
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/package.json +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { type SQL, sql } from "bun";
|
|
2
3
|
import { listApps } from "../../_internal/registry";
|
|
3
4
|
import {
|
|
@@ -25,7 +26,7 @@ export class OutgoingMailError extends Error {
|
|
|
25
26
|
constructor(
|
|
26
27
|
public readonly code: MailErrorCode,
|
|
27
28
|
message: string,
|
|
28
|
-
public readonly status: 400 | 403 | 404 | 409 | 502 = 400,
|
|
29
|
+
public readonly status: 400 | 403 | 404 | 409 | 502 | 503 = 400,
|
|
29
30
|
) {
|
|
30
31
|
super(message);
|
|
31
32
|
}
|
|
@@ -245,8 +246,8 @@ const appsState = async (
|
|
|
245
246
|
const registered = registeredApps ?? (await listApps());
|
|
246
247
|
const { access, grants } = await accessRows(db);
|
|
247
248
|
const [profile] = await db<{ key: string }[]>`SELECT key FROM outgoing_mail.profiles WHERE is_default`;
|
|
248
|
-
const logGrants = await db<{ app_id: string; source_app_id: string }[]>`
|
|
249
|
-
SELECT app_id, source_app_id FROM outgoing_mail.app_log_access ORDER BY source_app_id`;
|
|
249
|
+
const logGrants = await db<{ app_id: string; source_app_id: string; level: string }[]>`
|
|
250
|
+
SELECT app_id, source_app_id, level FROM outgoing_mail.app_log_access ORDER BY source_app_id`;
|
|
250
251
|
const ids = new Set([...registered.map((app) => app.id), ...access.map((a) => a.app_id), ...logGrants.map((g) => g.app_id)]);
|
|
251
252
|
const items = [...ids]
|
|
252
253
|
.map((appId): AdminMailApp => {
|
|
@@ -264,6 +265,12 @@ const appsState = async (
|
|
|
264
265
|
: logGrants
|
|
265
266
|
.filter((g) => g.app_id === appId && g.source_app_id !== "core" && g.source_app_id !== appId)
|
|
266
267
|
.map((g) => g.source_app_id),
|
|
268
|
+
logContentApps:
|
|
269
|
+
appId === "core"
|
|
270
|
+
? []
|
|
271
|
+
: logGrants
|
|
272
|
+
.filter((g) => g.app_id === appId && g.source_app_id !== "core" && g.source_app_id !== appId && g.level === "content")
|
|
273
|
+
.map((g) => g.source_app_id),
|
|
267
274
|
mode,
|
|
268
275
|
profiles: mode === "selected" ? grants.filter((g) => g.app_id === appId).map((g) => g.key) : [],
|
|
269
276
|
};
|
|
@@ -300,6 +307,7 @@ const setAppAccess = async (appId: string, value: MailAppAccess, context: MailAu
|
|
|
300
307
|
declared: false,
|
|
301
308
|
readDeclared: false,
|
|
302
309
|
logApps: [],
|
|
310
|
+
logContentApps: [],
|
|
303
311
|
mode: "default",
|
|
304
312
|
profiles: [],
|
|
305
313
|
}
|
|
@@ -312,19 +320,40 @@ const logAppsFor = async (appId: string): Promise<string[]> => {
|
|
|
312
320
|
WHERE app_id = ${appId} AND source_app_id <> 'core' AND source_app_id <> app_id ORDER BY source_app_id`;
|
|
313
321
|
return rows.map((row) => row.source_app_id);
|
|
314
322
|
};
|
|
323
|
+
/** Full exact IDs remain distinguishable after audit metadata string truncation. */
|
|
324
|
+
export const mailAppIdSha256 = (appId: string): string => createHash("sha256").update(appId, "utf8").digest("hex");
|
|
325
|
+
const grantAuditGroups = (grants: { source_app_id: string; level: string }[]) => {
|
|
326
|
+
const entries = grants.map((grant) => ({
|
|
327
|
+
appId: grant.source_app_id,
|
|
328
|
+
appIdSha256: mailAppIdSha256(grant.source_app_id),
|
|
329
|
+
level: grant.level,
|
|
330
|
+
}));
|
|
331
|
+
// Audit arrays retain at most 50 entries; the public grant limit is 100.
|
|
332
|
+
return [entries.slice(0, 50), entries.slice(50)];
|
|
333
|
+
};
|
|
315
334
|
const setAppLogAccess = async (appId: string, value: MailLogAccess, context: MailAuditContext): Promise<AdminMailApp> => {
|
|
316
335
|
const reader = MailLogAppIdSchema.safeParse(appId);
|
|
317
336
|
const parsed = MailLogAccessSchema.safeParse(value);
|
|
318
337
|
if (!reader.success || appId === "core" || !parsed.success || parsed.data.apps.includes(appId))
|
|
319
338
|
throw new OutgoingMailError("bad_input", "Choose a valid reader and distinct source apps other than Core or the reader itself.");
|
|
320
339
|
const apps = [...parsed.data.apps].sort();
|
|
340
|
+
const contentApps = parsed.data.contentApps ?? [];
|
|
321
341
|
const registered = await listApps();
|
|
322
342
|
return locked(async (tx) => {
|
|
343
|
+
const before = await tx<{ source_app_id: string; level: string }[]>`
|
|
344
|
+
SELECT source_app_id, level FROM outgoing_mail.app_log_access WHERE app_id = ${appId} ORDER BY source_app_id`;
|
|
323
345
|
await tx`DELETE FROM outgoing_mail.app_log_access WHERE app_id = ${appId}`;
|
|
324
|
-
await tx`INSERT INTO outgoing_mail.app_log_access(app_id, source_app_id, created_by)
|
|
325
|
-
SELECT ${appId}, source_app_id, ${context.actor.userId ?? null}
|
|
346
|
+
await tx`INSERT INTO outgoing_mail.app_log_access(app_id, source_app_id, created_by, level)
|
|
347
|
+
SELECT ${appId}, source_app_id, ${context.actor.userId ?? null},
|
|
348
|
+
CASE WHEN source_app_id = ANY(${toPgTextArray(contentApps)}::text[]) THEN 'content' ELSE 'metadata' END
|
|
326
349
|
FROM unnest(${toPgTextArray(apps)}::text[]) AS source_app_id`;
|
|
327
|
-
await record(tx, context, "app_log_access.update", appId, {
|
|
350
|
+
await record(tx, context, "app_log_access.update", appId, {
|
|
351
|
+
apps,
|
|
352
|
+
readingAppId: appId,
|
|
353
|
+
readingAppIdSha256: mailAppIdSha256(appId),
|
|
354
|
+
before: grantAuditGroups(before),
|
|
355
|
+
after: grantAuditGroups(apps.map((id) => ({ source_app_id: id, level: contentApps.includes(id) ? "content" : "metadata" }))),
|
|
356
|
+
});
|
|
328
357
|
return (
|
|
329
358
|
(await appsState(tx, registered)).items.find((app) => app.appId === appId) ?? {
|
|
330
359
|
appId,
|
|
@@ -333,6 +362,7 @@ const setAppLogAccess = async (appId: string, value: MailLogAccess, context: Mai
|
|
|
333
362
|
declared: false,
|
|
334
363
|
readDeclared: false,
|
|
335
364
|
logApps: [],
|
|
365
|
+
logContentApps: [],
|
|
336
366
|
mode: "default",
|
|
337
367
|
profiles: [],
|
|
338
368
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Socket } from "node:net";
|
|
1
2
|
import { audit } from "../audit";
|
|
3
|
+
import { DownstreamBusyError, downstreamBudget } from "../downstream-budget";
|
|
2
4
|
import { get } from "../settings";
|
|
3
5
|
import { type MailAuditContext, OutgoingMailError, resolveMailCredentials } from "./store";
|
|
4
6
|
import { resolveMailTransport } from "./transport";
|
|
@@ -18,22 +20,48 @@ export const outgoingMailTest = {
|
|
|
18
20
|
// Fail before the external effect if audit storage is unavailable.
|
|
19
21
|
await audit.record({ ...event, outcome: "allowed" });
|
|
20
22
|
let transport: ReturnType<Awaited<ReturnType<typeof resolveMailTransport>>["build"]> | undefined;
|
|
23
|
+
const signal = AbortSignal.timeout(60_000);
|
|
24
|
+
let socket: Socket | undefined;
|
|
25
|
+
const stop = () => {
|
|
26
|
+
socket?.destroy();
|
|
27
|
+
transport?.close();
|
|
28
|
+
};
|
|
29
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
30
|
+
let releaseSmtp: (() => void) | undefined;
|
|
21
31
|
try {
|
|
22
|
-
|
|
32
|
+
releaseSmtp = await downstreamBudget.acquire({ downstream: "smtp", timeoutMs: 60_000, signal });
|
|
33
|
+
const resolved = await resolveMailTransport(profile, signal);
|
|
34
|
+
const build = (index: number) => {
|
|
35
|
+
socket = new Socket();
|
|
36
|
+
const ownedSocket = socket;
|
|
37
|
+
ownedSocket.once("connect", () => {
|
|
38
|
+
if (signal.aborted) ownedSocket.destroy();
|
|
39
|
+
});
|
|
40
|
+
return resolved.build(index, ownedSocket);
|
|
41
|
+
};
|
|
42
|
+
let selected = 0;
|
|
23
43
|
for (let index = 0; index < resolved.count; index++) {
|
|
24
|
-
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
transport = build(index);
|
|
25
46
|
try {
|
|
26
47
|
// CONN also labels socket errors after DATA. Only fall back during a
|
|
27
48
|
// preflight that cannot send a message, then send once to that address.
|
|
28
49
|
await transport.verify();
|
|
50
|
+
selected = index;
|
|
29
51
|
break;
|
|
30
52
|
} catch (error) {
|
|
53
|
+
socket?.destroy();
|
|
31
54
|
transport.close();
|
|
32
55
|
transport = undefined;
|
|
33
56
|
if (!(error instanceof Error) || !("command" in error) || error.command !== "CONN" || index === resolved.count - 1) throw error;
|
|
34
57
|
}
|
|
35
58
|
}
|
|
36
59
|
if (!transport) throw new Error("No validated SMTP address.");
|
|
60
|
+
// verify() closes its SMTP connection. The actual send gets a fresh
|
|
61
|
+
// owned socket to the same checked address, with no delivery fallback.
|
|
62
|
+
stop();
|
|
63
|
+
signal.throwIfAborted();
|
|
64
|
+
transport = build(selected);
|
|
37
65
|
await transport.sendMail({
|
|
38
66
|
from: { address: profile.fromAddress, name: profile.fromName ?? (await get<string>("app.name")) },
|
|
39
67
|
to: recipient,
|
|
@@ -47,9 +75,11 @@ export const outgoingMailTest = {
|
|
|
47
75
|
profile.smtpUser ? `\0${profile.smtpUser}\0${profile.smtpPassword ?? ""}` : null,
|
|
48
76
|
]);
|
|
49
77
|
await audit.recordResultAfterSideEffect({ ...event, result: { ok: false, error: { code: "smtp_failed", message, status: 500 } } });
|
|
50
|
-
throw new OutgoingMailError("smtp_failed", message, 502);
|
|
78
|
+
throw new OutgoingMailError("smtp_failed", message, error instanceof DownstreamBusyError ? 503 : 502);
|
|
51
79
|
} finally {
|
|
52
|
-
|
|
80
|
+
signal.removeEventListener("abort", stop);
|
|
81
|
+
stop();
|
|
82
|
+
releaseSmtp?.();
|
|
53
83
|
}
|
|
54
84
|
},
|
|
55
85
|
};
|
|
@@ -4,7 +4,7 @@ import { resolveMailHost } from "./resolve-host";
|
|
|
4
4
|
import type { resolveMailCredentials } from "./store";
|
|
5
5
|
|
|
6
6
|
export const resolveMailTransport = async (profile: Awaited<ReturnType<typeof resolveMailCredentials>>, signal?: AbortSignal) => {
|
|
7
|
-
const target = await resolveMailHost(profile.smtpHost, signal);
|
|
7
|
+
const target = await resolveMailHost(profile.smtpHost, signal, undefined, profile.smtpPort);
|
|
8
8
|
signal?.throwIfAborted();
|
|
9
9
|
const count = target.addresses.length;
|
|
10
10
|
return {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { DownstreamBusyError, downstreamBudget } from "../downstream-budget";
|
|
1
2
|
import * as settings from "../settings";
|
|
2
3
|
import { offlineHtml } from "./offline-html";
|
|
3
4
|
|
|
4
5
|
export type GotenbergRenderErrorCode =
|
|
6
|
+
| "busy"
|
|
5
7
|
| "bad_input"
|
|
6
8
|
| "not_configured"
|
|
7
9
|
| "html_too_large"
|
|
@@ -117,6 +119,7 @@ const abortSignal = (timeoutMs: number): AbortSignal => {
|
|
|
117
119
|
|
|
118
120
|
const sanitizeFetchError = (error: unknown): GotenbergRenderError => {
|
|
119
121
|
if (error instanceof GotenbergRenderError) return error;
|
|
122
|
+
if (error instanceof DownstreamBusyError) return new GotenbergRenderError("busy", "PDF renderer is busy; retry later.", 503);
|
|
120
123
|
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
121
124
|
return new GotenbergRenderError("timeout", "Gotenberg request timed out.");
|
|
122
125
|
}
|
|
@@ -179,10 +182,14 @@ async function requestPdf(
|
|
|
179
182
|
const headers = new Headers();
|
|
180
183
|
const auth = basicAuthHeader(config);
|
|
181
184
|
if (auth) headers.set("Authorization", auth);
|
|
185
|
+
const baseUrl = normalizeBaseUrl(config.url);
|
|
182
186
|
const signal = options.signal ? AbortSignal.any([options.signal, abortSignal(config.timeoutMs)]) : abortSignal(config.timeoutMs);
|
|
187
|
+
let release: (() => void) | undefined;
|
|
183
188
|
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
184
189
|
try {
|
|
185
|
-
|
|
190
|
+
release = await downstreamBudget.acquire({ downstream: "gotenberg", timeoutMs: config.timeoutMs, signal });
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
const response = await (options.fetch ?? fetch)(`${baseUrl}${route}`, {
|
|
186
193
|
method: "POST",
|
|
187
194
|
headers,
|
|
188
195
|
body: form,
|
|
@@ -221,6 +228,7 @@ async function requestPdf(
|
|
|
221
228
|
throw sanitizeFetchError(error);
|
|
222
229
|
} finally {
|
|
223
230
|
reader?.releaseLock();
|
|
231
|
+
release?.();
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
|
|
@@ -285,47 +293,12 @@ export const mergePdfsWithConfig = async (
|
|
|
285
293
|
throw new GotenbergRenderError("bad_input", "At least one PDF file is required.");
|
|
286
294
|
}
|
|
287
295
|
|
|
288
|
-
const baseUrl = normalizeBaseUrl(config.url);
|
|
289
296
|
const form = new FormData();
|
|
290
297
|
input.files.forEach((file, index) => {
|
|
291
298
|
const filename = `${String(index + 1).padStart(6, "0")}.pdf`;
|
|
292
|
-
|
|
293
|
-
form.append("files", new Blob([bytes], { type: DEFAULT_PDF_CONTENT_TYPE }), filename);
|
|
299
|
+
form.append("files", new Blob([new Uint8Array(file.pdf)], { type: DEFAULT_PDF_CONTENT_TYPE }), filename);
|
|
294
300
|
});
|
|
295
|
-
|
|
296
|
-
const headers = new Headers();
|
|
297
|
-
const authHeader = basicAuthHeader(config);
|
|
298
|
-
if (authHeader) headers.set("Authorization", authHeader);
|
|
299
|
-
|
|
300
|
-
const transport = options.fetch ?? fetch;
|
|
301
|
-
let response: Response;
|
|
302
|
-
try {
|
|
303
|
-
response = await transport(`${baseUrl}/forms/pdfengines/merge`, {
|
|
304
|
-
method: "POST",
|
|
305
|
-
headers,
|
|
306
|
-
body: form,
|
|
307
|
-
signal: abortSignal(config.timeoutMs),
|
|
308
|
-
});
|
|
309
|
-
} catch (error) {
|
|
310
|
-
throw sanitizeFetchError(error);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (!response.ok) {
|
|
314
|
-
throw new GotenbergRenderError("bad_response", `Gotenberg returned HTTP ${response.status}.`, response.status);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const buffer = await response.arrayBuffer();
|
|
318
|
-
if (buffer.byteLength > config.maxPdfBytes) {
|
|
319
|
-
throw new GotenbergRenderError(
|
|
320
|
-
"pdf_too_large",
|
|
321
|
-
`PDF output is too large (${buffer.byteLength} bytes, limit ${config.maxPdfBytes} bytes).`,
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
pdf: new Uint8Array(buffer),
|
|
327
|
-
contentType: response.headers.get("content-type") || DEFAULT_PDF_CONTENT_TYPE,
|
|
328
|
-
};
|
|
301
|
+
return requestPdf("/forms/pdfengines/merge", form, config, options);
|
|
329
302
|
};
|
|
330
303
|
|
|
331
304
|
export const getGotenbergConfig = async (): Promise<GotenbergConfig> => ({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { markdownInfoBlocks, markdownLinkReference, renderMarkdownLink } from "@k2b/ui";
|
|
2
|
+
import { decodeHTML } from "entities";
|
|
2
3
|
import { Marked, Renderer, type Tokens } from "marked";
|
|
3
4
|
import postcss from "postcss";
|
|
4
5
|
import {
|
|
@@ -165,6 +166,161 @@ a.k2b-reference { padding: .05em .3em; border-radius: .3em; background: #eeeff2;
|
|
|
165
166
|
.k2b-reference__icon, .k2b-text-link__external { display: none; }
|
|
166
167
|
`;
|
|
167
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Break-anywhere lowers table column width to one character, splitting even
|
|
171
|
+
* short numbers. Cells keep words whole; long runs get explicit break points.
|
|
172
|
+
*/
|
|
173
|
+
const TABLE_CELL_PRINT_CSS = `
|
|
174
|
+
th, td { overflow-wrap: break-word; word-break: normal; }
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
// Up to 20 characters covers nearly every real word, including German
|
|
178
|
+
// compounds and IBAN-like values below that length. Longer URLs, code, hashes
|
|
179
|
+
// or paths need a 10-character step: even nine columns fit the document
|
|
180
|
+
// preset's narrowest row (about 90 table characters). A 20-character step
|
|
181
|
+
// clipped six columns of long inline code that fitted before this protection.
|
|
182
|
+
const MAX_WHOLE_CELL_RUN = 20;
|
|
183
|
+
const CELL_RUN_BREAK_STEP = 10;
|
|
184
|
+
|
|
185
|
+
// References (including semicolonless ones) and literal code points are raw
|
|
186
|
+
// pieces. Decode for whitespace and grapheme boundaries, but preserve spelling.
|
|
187
|
+
// Limit names to 32 characters, the longest HTML reference name.
|
|
188
|
+
const CHARACTER_REFERENCE = /&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[A-Za-z][A-Za-z0-9]{0,31});?/gu;
|
|
189
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
190
|
+
|
|
191
|
+
const runUnits = (pieces: { raw: string; decoded: string }[]): string[] => {
|
|
192
|
+
const decoded = pieces.map((piece) => piece.decoded).join("");
|
|
193
|
+
const ends = new Set(Array.from(GRAPHEMES.segment(decoded), ({ index, segment }) => index + segment.length));
|
|
194
|
+
const units: string[] = [];
|
|
195
|
+
let at = 0;
|
|
196
|
+
let unit = "";
|
|
197
|
+
// A boundary must end both a grapheme and a raw piece, even if it decodes
|
|
198
|
+
// to several graphemes (such as &x or an unknown reference-like name).
|
|
199
|
+
for (const piece of pieces) {
|
|
200
|
+
unit += piece.raw;
|
|
201
|
+
at += piece.decoded.length;
|
|
202
|
+
if (ends.has(at)) {
|
|
203
|
+
units.push(unit);
|
|
204
|
+
unit = "";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return units;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Inline formatting does not end a run: `aaa**bbb**ccc` prints one unbroken
|
|
211
|
+
// word. Any other element, such as <br>, <wbr>, an image, a span, or a
|
|
212
|
+
// block, starts a new run, so a short value beside it never breaks.
|
|
213
|
+
const INLINE_FORMATTING = new Set([
|
|
214
|
+
"a",
|
|
215
|
+
"b",
|
|
216
|
+
"code",
|
|
217
|
+
"del",
|
|
218
|
+
"em",
|
|
219
|
+
"i",
|
|
220
|
+
"ins",
|
|
221
|
+
"kbd",
|
|
222
|
+
"mark",
|
|
223
|
+
"s",
|
|
224
|
+
"samp",
|
|
225
|
+
"small",
|
|
226
|
+
"strong",
|
|
227
|
+
"sub",
|
|
228
|
+
"sup",
|
|
229
|
+
"u",
|
|
230
|
+
"var",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
// Graphics and formulas keep their own layout. In raw-text and RCDATA elements
|
|
234
|
+
// a <wbr> would be literal text, not markup.
|
|
235
|
+
const SKIPPED_IN_CELLS = new Set([
|
|
236
|
+
"svg",
|
|
237
|
+
"math",
|
|
238
|
+
"style",
|
|
239
|
+
"script",
|
|
240
|
+
"textarea",
|
|
241
|
+
"title",
|
|
242
|
+
"xmp",
|
|
243
|
+
"iframe",
|
|
244
|
+
"noembed",
|
|
245
|
+
"noframes",
|
|
246
|
+
"noscript",
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const breakLongCellRuns = (html: string): string => {
|
|
250
|
+
let skippedDepth = 0;
|
|
251
|
+
let buffer = "";
|
|
252
|
+
// Units of the run that the previous text node in the cell ended with.
|
|
253
|
+
let carried = 0;
|
|
254
|
+
const pieces = new RegExp(`(${CHARACTER_REFERENCE.source})|.`, "gsu");
|
|
255
|
+
const breakText = (text: string): string => {
|
|
256
|
+
let output = "";
|
|
257
|
+
let run: { raw: string; decoded: string }[] = [];
|
|
258
|
+
const flushRun = () => {
|
|
259
|
+
const units = runUnits(run);
|
|
260
|
+
run = [];
|
|
261
|
+
const start = carried;
|
|
262
|
+
carried += units.length;
|
|
263
|
+
// A run that continues in a later text node is counted when it gets
|
|
264
|
+
// there; earlier nodes stay as written, so at most 30 units stay whole.
|
|
265
|
+
if (carried <= MAX_WHOLE_CELL_RUN) {
|
|
266
|
+
output += units.join("");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
output += units
|
|
270
|
+
.map((unit, index) => ((start + index) % CELL_RUN_BREAK_STEP === 0 && start + index > 0 ? `<wbr>${unit}` : unit))
|
|
271
|
+
.join("");
|
|
272
|
+
};
|
|
273
|
+
for (const [raw, reference] of text.matchAll(pieces)) {
|
|
274
|
+
const decoded = reference === undefined ? raw : decodeHTML(raw);
|
|
275
|
+
// Literal and referenced whitespace end runs, except no-break characters.
|
|
276
|
+
if (/^[^\S\u00a0\u2007\u202f\ufeff]+$/u.test(decoded)) {
|
|
277
|
+
flushRun();
|
|
278
|
+
carried = 0;
|
|
279
|
+
output += raw;
|
|
280
|
+
} else {
|
|
281
|
+
run.push({ raw, decoded });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
flushRun();
|
|
285
|
+
return output;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return new HTMLRewriter()
|
|
289
|
+
.on("td *, th *", {
|
|
290
|
+
element(element) {
|
|
291
|
+
if (INLINE_FORMATTING.has(element.tagName)) return;
|
|
292
|
+
carried = 0;
|
|
293
|
+
// Void elements and self-closing foreign elements have no end tag. HTML
|
|
294
|
+
// ignores the self-closing flag elsewhere: <textarea/> still opens raw text.
|
|
295
|
+
if (!element.canHaveContent) return;
|
|
296
|
+
const skipped = SKIPPED_IN_CELLS.has(element.tagName);
|
|
297
|
+
if (skipped) skippedDepth += 1;
|
|
298
|
+
// One end tag handler per element: a second registration would replace this one.
|
|
299
|
+
element.onEndTag(() => {
|
|
300
|
+
carried = 0;
|
|
301
|
+
if (skipped) skippedDepth -= 1;
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
.on("td, th", {
|
|
306
|
+
element() {
|
|
307
|
+
carried = 0;
|
|
308
|
+
},
|
|
309
|
+
text(chunk) {
|
|
310
|
+
if (skippedDepth) return;
|
|
311
|
+
// HTMLRewriter may split one text node (even an entity) into chunks.
|
|
312
|
+
buffer += chunk.text;
|
|
313
|
+
if (chunk.lastInTextNode) {
|
|
314
|
+
chunk.replace(breakText(buffer), { html: true });
|
|
315
|
+
buffer = "";
|
|
316
|
+
} else {
|
|
317
|
+
chunk.remove();
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
.transform(html);
|
|
322
|
+
};
|
|
323
|
+
|
|
168
324
|
const byteLength = (value: string): number => new TextEncoder().encode(value).byteLength;
|
|
169
325
|
|
|
170
326
|
const escapeHtml = (value: string): string =>
|
|
@@ -258,7 +414,9 @@ export const buildPresetPdfHtml = (input: BuildPresetPdfHtmlInput): string => {
|
|
|
258
414
|
const suppliedCustomCss = input.customCss?.trim() ?? "";
|
|
259
415
|
const customCss = suppliedCustomCss ? validateCustomCss(input.customCss ?? "") : "";
|
|
260
416
|
const presetCss = templateId ? TEMPLATE_CSS[templateId] : customCss ? "" : TEMPLATE_CSS.document;
|
|
261
|
-
const baseCss = [presetCss, INFO_BLOCK_PRINT_CSS, LINK_PRINT_CSS, input.css ? styleText(input.css) : ""]
|
|
417
|
+
const baseCss = [presetCss, INFO_BLOCK_PRINT_CSS, LINK_PRINT_CSS, TABLE_CELL_PRINT_CSS, input.css ? styleText(input.css) : ""]
|
|
418
|
+
.filter(Boolean)
|
|
419
|
+
.join("\n");
|
|
262
420
|
const stylesheet = `${baseCss}${baseCss && customCss ? `\n/* Custom CSS overrides */\n` : ""}${customCss}`;
|
|
263
421
|
return `<!doctype html>
|
|
264
422
|
<html lang="en">
|
|
@@ -268,7 +426,7 @@ export const buildPresetPdfHtml = (input: BuildPresetPdfHtmlInput): string => {
|
|
|
268
426
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; connect-src 'none'; font-src 'none'; form-action 'none'; frame-src 'none'; img-src 'none'; media-src 'none'; object-src 'none'; script-src 'none'; style-src 'unsafe-inline'">
|
|
269
427
|
<style>${stylesheet}</style>
|
|
270
428
|
</head>
|
|
271
|
-
<body><main class="markdown-document">${input.html}</main></body>
|
|
429
|
+
<body><main class="markdown-document">${breakLongCellRuns(input.html)}</main></body>
|
|
272
430
|
</html>`;
|
|
273
431
|
};
|
|
274
432
|
|
|
@@ -47,7 +47,7 @@ export type RenderTemplatePdfPreviewOptions = RenderHtmlToPdfOptions & {
|
|
|
47
47
|
config?: GotenbergConfig;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const pdfErrorStatus = (error: GotenbergRenderError): number => (error.code === "not_configured" ? 400 : 502);
|
|
50
|
+
const pdfErrorStatus = (error: GotenbergRenderError): number => (error.code === "busy" ? 503 : error.code === "not_configured" ? 400 : 502);
|
|
51
51
|
|
|
52
52
|
const renderPart = (
|
|
53
53
|
template: string | null | undefined,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClientRequest, IncomingMessage } from "node:http";
|
|
2
2
|
import { type RequestOptions, request } from "node:https";
|
|
3
3
|
import { resolvePublicNetworkAddresses } from "./network-security";
|
|
4
|
+
import { awaitNetworkOperation, readNetworkResponse } from "./network-transport";
|
|
4
5
|
|
|
5
6
|
export type PublicHttpInput = {
|
|
6
7
|
url: string;
|
|
@@ -24,29 +25,21 @@ export async function requestPublicHttps(
|
|
|
24
25
|
const url = new URL(input.url);
|
|
25
26
|
if (url.protocol !== "https:" || url.username || url.password) throw new Error("HTTPS_REQUIRED");
|
|
26
27
|
input.signal.throwIfAborted();
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
dependencies.resolve(url.hostname),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}),
|
|
35
|
-
]).finally(() => input.signal.removeEventListener("abort", cancelLookup));
|
|
28
|
+
let addresses: Awaited<ReturnType<typeof resolvePublicNetworkAddresses>>;
|
|
29
|
+
try {
|
|
30
|
+
addresses = await awaitNetworkOperation(dependencies.resolve(url.hostname), input.signal);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (input.signal.aborted) throw new Error("HTTP_CANCELLED");
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
36
35
|
input.signal.throwIfAborted();
|
|
37
36
|
const target = addresses[0]!;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
input.signal.removeEventListener("abort", abort);
|
|
45
|
-
if (value) resolve(value);
|
|
46
|
-
else reject(new Error("HTTP_FAILED"));
|
|
47
|
-
};
|
|
48
|
-
const outgoing = dependencies.request(
|
|
49
|
-
{
|
|
37
|
+
try {
|
|
38
|
+
return await readNetworkResponse<{ status: number; headers: Record<string, string>; body: Uint8Array }>({
|
|
39
|
+
request: dependencies.request,
|
|
40
|
+
signal: input.signal,
|
|
41
|
+
body: input.body,
|
|
42
|
+
options: {
|
|
50
43
|
hostname: target.address,
|
|
51
44
|
family: target.family,
|
|
52
45
|
port: url.port || 443,
|
|
@@ -56,7 +49,7 @@ export async function requestPublicHttps(
|
|
|
56
49
|
method: input.method,
|
|
57
50
|
headers: { ...input.headers, host: url.host, "accept-encoding": "identity" },
|
|
58
51
|
},
|
|
59
|
-
(incoming) => {
|
|
52
|
+
read: (incoming, finish, fail) => {
|
|
60
53
|
const status = incoming.statusCode ?? 0;
|
|
61
54
|
const headers: Record<string, string> = {};
|
|
62
55
|
for (const name of ["content-type", "retry-after", "etag", "last-modified"]) {
|
|
@@ -65,37 +58,32 @@ export async function requestPublicHttps(
|
|
|
65
58
|
}
|
|
66
59
|
// A redirect never receives a second request or exposes a Location token.
|
|
67
60
|
if (status >= 300 && status < 400 && status !== 304) {
|
|
68
|
-
incoming.destroy();
|
|
69
61
|
finish({ status, headers, body: new Uint8Array() });
|
|
70
62
|
return;
|
|
71
63
|
}
|
|
72
64
|
if (incoming.headers["content-encoding"] && incoming.headers["content-encoding"] !== "identity") {
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
fail(new Error("HTTP_FAILED"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (Number(incoming.headers["content-length"]) > input.maxBytes) {
|
|
69
|
+
fail(new Error("HTTP_TOO_LARGE"));
|
|
75
70
|
return;
|
|
76
71
|
}
|
|
77
72
|
const chunks: Uint8Array[] = [];
|
|
78
73
|
let bytes = 0;
|
|
79
74
|
incoming.on("data", (chunk: Buffer) => {
|
|
80
|
-
if (
|
|
75
|
+
if (incoming.destroyed) return;
|
|
81
76
|
bytes += chunk.byteLength;
|
|
82
77
|
if (bytes > input.maxBytes) {
|
|
83
|
-
|
|
84
|
-
fail();
|
|
78
|
+
fail(new Error("HTTP_TOO_LARGE"));
|
|
85
79
|
} else chunks.push(chunk);
|
|
86
80
|
});
|
|
87
81
|
incoming.on("end", () => finish({ status, headers, body: Buffer.concat(chunks, bytes) }));
|
|
88
|
-
incoming.on("error", fail);
|
|
89
|
-
incoming.on("aborted", fail);
|
|
90
82
|
},
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
input.signal.addEventListener("abort", abort, { once: true });
|
|
98
|
-
if (input.signal.aborted) abort();
|
|
99
|
-
else outgoing.end(input.body);
|
|
100
|
-
});
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof Error && error.message === "HTTP_TOO_LARGE") throw error;
|
|
86
|
+
// Never expose errors containing injected headers.
|
|
87
|
+
throw new Error("HTTP_FAILED");
|
|
88
|
+
}
|
|
101
89
|
}
|