@k2b/cloud 0.6.0 → 0.8.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/README.md +2 -2
- package/package.json +22 -11
- package/scripts/README.md +10 -0
- package/scripts/browser-performance.ts +14 -0
- package/scripts/build-pdf-renderer.ts +39 -0
- package/scripts/build.ts +20 -35
- package/scripts/preload.ts +16 -12
- package/scripts/sync-recovery-smoke.ts +22 -6
- package/src/_internal/app-presentation.ts +14 -2
- package/src/_internal/build-metadata.ts +8 -1
- package/src/_internal/capabilities.ts +131 -7
- package/src/_internal/capability-streams.ts +81 -0
- package/src/_internal/define-app.ts +80 -37
- package/src/_internal/heartbeat.ts +4 -0
- package/src/_internal/help-catalog.ts +27 -172
- package/src/_internal/help.ts +13 -76
- package/src/_internal/page-responses.ts +2 -0
- package/src/_internal/postgres-application-name.ts +30 -0
- package/src/_internal/process-identity.ts +15 -0
- package/src/_internal/process-sync.ts +1 -1
- package/src/_internal/readiness.ts +4 -1
- package/src/_internal/registry-validation.ts +29 -25
- package/src/_internal/registry.ts +2 -53
- package/src/_internal/runtime-context.ts +5 -16
- package/src/_internal/server-timing.ts +13 -0
- package/src/_internal/status-preserving-ssr.ts +18 -6
- package/src/_internal/web-vitals-asset.ts +10 -0
- package/src/access/PermissionEditor.tsx +18 -151
- package/src/access/PrincipalPicker.tsx +110 -0
- package/src/access/ResourceApiKeys.tsx +1 -1
- package/src/access/ui.ts +1 -0
- package/src/ai/admin.ts +14 -13
- package/src/ai/assistant-models.ts +12 -1
- package/src/ai/audio-format.ts +34 -0
- package/src/ai/audio-tool.ts +90 -0
- package/src/ai/browser-code-contracts.ts +201 -0
- package/src/ai/browser.ts +4 -0
- package/src/ai/capabilities.ts +55 -36
- package/src/ai/capability-execution.ts +47 -12
- package/src/ai/chat/blocks.tsx +229 -90
- package/src/ai/chat/builtin-tools.tsx +2 -4
- package/src/ai/chat/capability-result.ts +15 -0
- package/src/ai/chat/capability-table.tsx +2 -10
- package/src/ai/chat/composer-adapter.ts +83 -3
- package/src/ai/chat/message-actions.tsx +8 -7
- package/src/ai/chat/message-utils.ts +25 -1
- package/src/ai/chat/messages.ts +2 -0
- package/src/ai/chat/presentation.tsx +31 -47
- package/src/ai/chat/primitives.tsx +43 -1
- package/src/ai/chat/tool-disclosure.tsx +21 -2
- package/src/ai/chat/tool-groups.ts +58 -0
- package/src/ai/chat-quotas.ts +25 -0
- package/src/ai/chat-task-contracts.ts +14 -10
- package/src/ai/chat-tasks.ts +297 -43
- package/src/ai/client/controller.ts +165 -39
- package/src/ai/client/file-source.ts +12 -6
- package/src/ai/client/live-connection.ts +1 -0
- package/src/ai/client/projection.ts +29 -11
- package/src/ai/client/transport.ts +6 -0
- package/src/ai/code-capability-routes.ts +105 -0
- package/src/ai/code-capability-transport.ts +39 -0
- package/src/ai/code-execution.ts +53 -0
- package/src/ai/code-mode-skill.ts +118 -0
- package/src/ai/code-runtime-tools.ts +179 -0
- package/src/ai/code-source-contracts.ts +270 -0
- package/src/ai/code-source-tools.ts +188 -0
- package/src/ai/compaction.ts +27 -6
- package/src/ai/data-analysis-skill.ts +10 -0
- package/src/ai/default-tools.ts +99 -3
- package/src/ai/dictation-runtime.ts +171 -0
- package/src/ai/dictations.ts +188 -0
- package/src/ai/draft-content.ts +49 -0
- package/src/ai/enrich.ts +6 -0
- package/src/ai/executor.ts +189 -33
- package/src/ai/fetch-file-tool.ts +2 -2
- package/src/ai/file-content-version.ts +27 -0
- package/src/ai/file-context.ts +2 -1
- package/src/ai/file-media-type.ts +12 -0
- package/src/ai/file-reference-contracts.ts +15 -0
- package/src/ai/file-tools.ts +8 -1
- package/src/ai/files-store.ts +212 -71
- package/src/ai/fixtures/vision-encrypted.pdf +0 -0
- package/src/ai/grids-skill.ts +42 -10
- package/src/ai/http.ts +26 -8
- package/src/ai/index.ts +26 -3
- package/src/ai/inference-calls.ts +197 -0
- package/src/ai/live-events.ts +5 -1
- package/src/ai/live.ts +1 -1
- package/src/ai/markdown-pdf-tool.ts +2 -2
- package/src/ai/memories.ts +30 -27
- package/src/ai/memory-learning-runs.ts +1 -1
- package/src/ai/memory-learning.ts +64 -32
- package/src/ai/memory-tool.ts +1 -5
- package/src/ai/memory-workflow-evidence.ts +9 -4
- package/src/ai/message-queue.ts +178 -0
- package/src/ai/migrate.ts +189 -11
- package/src/ai/model-pricing.ts +43 -0
- package/src/ai/pdf-render-worker.ts +75 -0
- package/src/ai/pdf-render.fixture.ts +28 -0
- package/src/ai/pdf-render.ts +77 -0
- package/src/ai/projects-routes.ts +18 -43
- package/src/ai/projects.ts +33 -14
- package/src/ai/protocol.ts +3 -0
- package/src/ai/provider.ts +1 -8
- package/src/ai/quota-provider.ts +192 -0
- package/src/ai/quota-report.ts +152 -0
- package/src/ai/quotas-migrate.ts +45 -0
- package/src/ai/quotas.ts +168 -0
- package/src/ai/routes.ts +241 -11
- package/src/ai/run-timeout.ts +12 -0
- package/src/ai/runtime-tools.ts +7 -5
- package/src/ai/runtime.ts +74 -17
- package/src/ai/settings.ts +36 -8
- package/src/ai/short-id.ts +4 -5
- package/src/ai/skill-catalog.ts +0 -10
- package/src/ai/skill-search.ts +58 -0
- package/src/ai/skill-seeds.ts +114 -20
- package/src/ai/skill-tool.ts +21 -5
- package/src/ai/skills-routes.ts +49 -4
- package/src/ai/skills.ts +358 -163
- package/src/ai/store.ts +416 -201
- package/src/ai/stream.ts +5 -0
- package/src/ai/structured-runs.ts +4 -3
- package/src/ai/structured.ts +12 -1
- package/src/ai/system-prompt.ts +14 -2
- package/src/ai/task-contracts.ts +84 -0
- package/src/ai/task-execution.ts +92 -0
- package/src/ai/timeline.ts +2 -5
- package/src/ai/todo-contracts.ts +32 -0
- package/src/ai/todo-tool.ts +19 -0
- package/src/ai/tool-audit.ts +30 -40
- package/src/ai/tools.ts +6 -0
- package/src/ai/transcription.ts +202 -0
- package/src/ai/turn-timing.ts +114 -0
- package/src/ai/types.ts +58 -10
- package/src/ai/ui.tsx +3 -0
- package/src/ai/usage.ts +118 -118
- package/src/ai/vision-tool.ts +60 -10
- package/src/api/admin-ai-projects.ts +2 -2
- package/src/api/admin-ai-quotas.ts +80 -0
- package/src/api/admin-ai-skills.ts +48 -5
- package/src/api/admin-ai-usage.ts +2 -2
- package/src/api/admin-core-settings.ts +25 -0
- package/src/api/admin-identity.ts +91 -51
- package/src/api/admin-linux-identities.ts +10 -4
- package/src/api/admin-rail.ts +58 -0
- package/src/api/announcements.ts +14 -0
- package/src/api/app-approval.ts +2 -2
- package/src/api/auth.ts +1 -1
- package/src/api/capabilities.ts +324 -43
- package/src/api/capability-streams.ts +262 -0
- package/src/api/help.ts +8 -44
- package/src/api/index.ts +8 -2
- package/src/api/mcp.ts +45 -34
- package/src/api/me-web-vitals.ts +35 -0
- package/src/api/me.ts +2 -0
- package/src/api/search/schemas.ts +26 -15
- package/src/api/search.bench.ts +1 -1
- package/src/api/search.ts +54 -17
- package/src/browser/CloudResourceSearch.tsx +640 -702
- package/src/browser/app-approval-vault.ts +19 -7
- package/src/browser/command-bridge.ts +109 -0
- package/src/browser/command-shortcuts.ts +76 -0
- package/src/browser/commands.ts +67 -0
- package/src/browser/navigation-search.ts +48 -0
- package/src/browser/notifications.ts +1 -1
- package/src/browser/resource-picker.tsx +5 -18
- package/src/browser/resource-search-dialog.ts +9 -0
- package/src/browser/resource-search-input.ts +46 -0
- package/src/browser/resource-search-messages.ts +76 -40
- package/src/browser/resource-search.ts +7 -0
- package/src/browser/search-bridge.ts +102 -0
- package/src/browser/search-commands.ts +59 -0
- package/src/browser/search.ts +13 -0
- package/src/browser/spotlight-position.ts +208 -0
- package/src/browser/testing.ts +6 -0
- package/src/browser/web-vitals.ts +25 -0
- package/src/capabilities/claims.ts +179 -0
- package/src/capabilities/client.ts +34 -0
- package/src/capabilities/command-link.ts +16 -0
- package/src/capabilities/executions.ts +298 -0
- package/src/capabilities/migrate.ts +81 -0
- package/src/capabilities/server.ts +116 -35
- package/src/capabilities/stream-body.ts +16 -0
- package/src/capabilities/streams.ts +28 -0
- package/src/cli/admin/ai-quotas.ts +161 -0
- package/src/cli/admin/ai-skills.ts +64 -0
- package/src/cli/admin/ai-usage.ts +53 -30
- package/src/cli/admin/app-credentials.ts +77 -0
- package/src/cli/admin/data.ts +17 -0
- package/src/cli/admin/index.ts +10 -1
- package/src/cli/admin/instance.ts +2 -2
- package/src/cli/admin/linux.ts +4 -2
- package/src/cli/admin/sync.ts +1 -1
- package/src/cli/capabilities.ts +42 -0
- package/src/cli/capability-streams.ts +25 -0
- package/src/cli/index.ts +3 -2
- package/src/config/define-env.ts +97 -0
- package/src/config/env.ts +147 -51
- package/src/config/index.ts +3 -1
- package/src/contracts/app.ts +23 -3
- package/src/contracts/capabilities.ts +75 -5
- package/src/contracts/capability-streams.ts +46 -0
- package/src/contracts/commands.ts +49 -0
- package/src/contracts/index.ts +5 -2
- package/src/contracts/rail-admin.ts +38 -0
- package/src/contracts/rail-preferences.ts +16 -0
- package/src/contracts/registry.ts +5 -27
- package/src/contracts/settings-types.ts +1 -1
- package/src/contracts/shared.ts +18 -5
- package/src/contracts/web-vitals.ts +27 -0
- package/src/desktop/index.ts +1 -0
- package/src/desktop/solid.tsx +1 -1
- package/src/index.ts +8 -6
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +3 -2
- package/src/server/middleware/route-template.ts +9 -3
- package/src/server/middleware/runtime.ts +2 -1
- package/src/server/middleware/settings.ts +17 -11
- package/src/server/ratelimit.ts +1 -1
- package/src/server/services/access-revision.ts +10 -0
- package/src/server/services/access.ts +3 -1
- package/src/server/services/index.ts +1 -0
- package/src/services/account-category-policy.ts +9 -0
- package/src/services/account-lifecycle/audit.ts +1 -1
- package/src/services/accounts/app.ts +12 -2
- package/src/services/accounts/email-write.ts +23 -0
- package/src/services/accounts/groups.ts +15 -4
- package/src/services/accounts/identities.ts +249 -0
- package/src/services/accounts/identity-reconciliation.ts +87 -0
- package/src/services/accounts/ipa-data.ts +2 -2
- package/src/services/accounts/local-groups.ts +2 -2
- package/src/services/accounts/notification-sender.ts +12 -2
- package/src/services/accounts/posix.ts +45 -29
- package/src/services/accounts/users.ts +2 -2
- package/src/services/announcements/index.ts +69 -8
- package/src/services/app-approval.ts +11 -10
- package/src/services/audit/index.ts +1 -1
- package/src/services/cache-fill.ts +35 -0
- package/src/services/help/index.ts +109 -0
- package/src/services/help/maintenance.ts +25 -0
- package/src/services/help/store.ts +180 -0
- package/src/services/help/types.ts +46 -0
- package/src/services/identity/invocation-actor.ts +2 -2
- package/src/services/identity/key-config.ts +8 -5
- package/src/services/identity/key-ring.ts +1 -1
- package/src/services/identity/runtime-config.ts +6 -5
- package/src/services/index.ts +15 -0
- package/src/services/ipa/posix.ts +1 -1
- package/src/services/ipa/sync.ts +1 -1
- package/src/services/ipa/users.ts +268 -221
- package/src/services/legal-consent.ts +1 -1
- package/src/services/logging/index.ts +15 -13
- package/src/services/logging/trace.ts +2 -1
- package/src/services/logging/web-vitals.ts +81 -0
- package/src/services/mandates/index.ts +21 -4
- package/src/services/mandates/policy.ts +165 -2
- package/src/services/notifications/batches.ts +125 -57
- package/src/services/notifications/browser.ts +12 -0
- package/src/services/notifications/index.ts +0 -2
- package/src/services/notifications/platform.ts +0 -4
- package/src/services/oauth-tokens.ts +3 -3
- package/src/services/pdf/gotenberg.ts +131 -72
- package/src/services/pdf/index.ts +11 -8
- package/src/services/providers/local/users.ts +25 -16
- package/src/services/public-http.ts +101 -0
- package/src/services/rail-shortcuts.ts +95 -0
- package/src/services/rail-snapshot.ts +53 -0
- package/src/services/request-cache-redis.ts +29 -0
- package/src/services/session/index.ts +15 -2
- package/src/services/session/user.ts +62 -37
- package/src/services/settings/app.ts +5 -1
- package/src/services/settings/core-settings.ts +26 -35
- package/src/services/settings/defaults.ts +5 -0
- package/src/services/settings/index.ts +3 -3
- package/src/services/settings/store.ts +58 -84
- package/src/services/webauthn.ts +1 -1
- package/src/shared/ai-costs.ts +42 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/ai-quotas.ts +136 -0
- package/src/shared/ai-usage.ts +6 -16
- package/src/shared/app-presentation.ts +21 -15
- package/src/shared/capability-messages.ts +14 -0
- package/src/shared/help.ts +6 -42
- package/src/shared/index.ts +3 -0
- package/src/shared/markdown/extensions/info-blocks.ts +1 -1
- package/src/ssr/AdminLayout.tsx +7 -5
- package/src/ssr/AdminSidebar.tsx +30 -16
- package/src/ssr/AppLaunchpad.island.tsx +33 -136
- package/src/ssr/AppLaunchpadPanel.tsx +181 -0
- package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
- package/src/ssr/GlobalSearchDialog.tsx +167 -20
- package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
- package/src/ssr/Layout.tsx +17 -6
- package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +46 -14
- package/src/ssr/LayoutHelp.tsx +22 -23
- package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
- package/src/ssr/LayoutRail.tsx +17 -9
- package/src/ssr/MinimalLayout.tsx +1 -1
- package/src/ssr/MobileNavigation.tsx +71 -0
- package/src/ssr/MobileProfileActions.tsx +49 -0
- package/src/ssr/ProfilePreferences.island.tsx +3 -36
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/RailEditor.tsx +17 -2
- package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
- package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
- package/src/ssr/admin-navigation.ts +11 -1
- package/src/ssr/help.ts +29 -0
- package/src/ssr/islands/SearchBar.tsx +67 -0
- package/src/ssr/islands/index.ts +3 -1
- package/src/ssr/layout-context.ts +2 -2
- package/src/ssr/layout-help-search.ts +15 -0
- package/src/ssr/mobile-menu-history.ts +70 -0
- package/src/ssr/platform-messages.ts +16 -8
- package/src/ssr/profile-actions.ts +42 -0
- package/src/ssr/rail-context.ts +8 -4
- package/src/ssr/rail-messages.ts +2 -0
- package/src/ssr/rail-navigation.ts +3 -3
- package/src/ssr/workspace-navigation.ts +75 -0
- package/src/styles/effects.css +6 -6
- package/src/styles/global.css +54 -0
- package/src/styles/input.css +43 -41
- package/src/styles/resource-search.css +505 -0
- package/src/styles/utilities-feedback.css +13 -0
- package/src/styles/utilities-layout.css +1 -5
- package/src/styles/utilities-navigation.css +3 -39
- package/src/types/ambient.d.ts +5 -0
- package/src/workflows/ai/index.ts +2 -2
- package/src/workflows/ai/runtime.ts +19 -90
- package/src/workflows/ai/store.ts +2 -0
- package/src/workflows/ai/types.ts +6 -82
- package/src/workflows/ai-actions.ts +16 -13
- package/src/workflows/definition.ts +10 -5
- package/src/workflows/store/actions.ts +44 -20
- package/src/workflows/store/index.ts +4 -2
- package/src/workflows/store/runs.ts +36 -0
- package/src/workflows/store/worker-pool.ts +55 -0
- package/src/workflows/store/worker-runtime.ts +90 -0
- package/src/ai/kit-skill.ts +0 -22
- package/src/ai/usage-migrate.ts +0 -81
- package/src/browser/notification-target.ts +0 -27
- package/src/contracts/notification-live.ts +0 -66
- package/src/server/services/freeipa/test-certificates.ts +0 -44
- package/src/services/notifications/live.ts +0 -50
- package/src/services/session/test-fixture.ts +0 -18
- package/src/ssr/BrowserNotifications.island.tsx +0 -135
- package/src/ssr/islands/SearchBar.island.tsx +0 -80
|
@@ -4,6 +4,7 @@ import type { Worker } from "@k2b/sync";
|
|
|
4
4
|
import { sql } from "bun";
|
|
5
5
|
import { lazySync } from "../../_internal/process-sync";
|
|
6
6
|
import { markdown } from "../../shared/markdown";
|
|
7
|
+
import { type AuditActor, audit } from "../audit";
|
|
7
8
|
import { logger, trace } from "../logging";
|
|
8
9
|
import { parsePgJsonValue, toPgTextArray, toPgUuidArray } from "../postgres";
|
|
9
10
|
|
|
@@ -406,7 +407,7 @@ export const createDraft = async (params: {
|
|
|
406
407
|
subject: string;
|
|
407
408
|
bodyMarkdown: string;
|
|
408
409
|
selection: NotificationBatchSelection;
|
|
409
|
-
|
|
410
|
+
actor: AuditActor & { userId: string };
|
|
410
411
|
}): Promise<Result<NotificationBatch>> => {
|
|
411
412
|
const subject = params.subject.trim();
|
|
412
413
|
const bodyMarkdown = params.bodyMarkdown.trim();
|
|
@@ -422,12 +423,24 @@ export const createDraft = async (params: {
|
|
|
422
423
|
}
|
|
423
424
|
const hash = selectionHash(selection);
|
|
424
425
|
const bodyHtml = markdown.renderSync(bodyMarkdown);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
return sql.begin(async (tx) => {
|
|
427
|
+
const rows = await tx<BatchRow[]>`
|
|
428
|
+
INSERT INTO notifications.batches (subject, body_markdown, body_html, selection, selection_hash, created_by)
|
|
429
|
+
VALUES (${subject}, ${bodyMarkdown}, ${bodyHtml}, ${JSON.stringify(selection)}::text::jsonb, ${hash}, ${params.actor.userId}::uuid)
|
|
430
|
+
RETURNING *
|
|
431
|
+
`;
|
|
432
|
+
const batch = mapBatch(rows[0]!);
|
|
433
|
+
await audit.record(
|
|
434
|
+
{
|
|
435
|
+
action: "accounts.notification_batch.create",
|
|
436
|
+
outcome: "allowed",
|
|
437
|
+
actor: params.actor,
|
|
438
|
+
target: { type: "notification_batch", id: batch.id },
|
|
439
|
+
},
|
|
440
|
+
tx,
|
|
441
|
+
);
|
|
442
|
+
return ok(batch);
|
|
443
|
+
});
|
|
431
444
|
};
|
|
432
445
|
|
|
433
446
|
export const get = async (id: string): Promise<NotificationBatch | null> => {
|
|
@@ -504,7 +517,7 @@ export const listRecipients = async (params: {
|
|
|
504
517
|
|
|
505
518
|
export const finalize = async (params: {
|
|
506
519
|
id: string;
|
|
507
|
-
|
|
520
|
+
actor: AuditActor & { userId: string };
|
|
508
521
|
expectedSelectionHash: string;
|
|
509
522
|
expectedDeliverableCount: number;
|
|
510
523
|
expectedRecipientHash: string;
|
|
@@ -568,7 +581,7 @@ export const finalize = async (params: {
|
|
|
568
581
|
UPDATE notifications.batches
|
|
569
582
|
SET
|
|
570
583
|
status = 'ready',
|
|
571
|
-
finalized_by = ${params.
|
|
584
|
+
finalized_by = ${params.actor.userId}::uuid,
|
|
572
585
|
finalized_at = now(),
|
|
573
586
|
target_count = ${candidates.length},
|
|
574
587
|
deliverable_count = ${deliverableCount},
|
|
@@ -576,6 +589,16 @@ export const finalize = async (params: {
|
|
|
576
589
|
WHERE id = ${params.id}::uuid
|
|
577
590
|
RETURNING *
|
|
578
591
|
`;
|
|
592
|
+
await audit.record(
|
|
593
|
+
{
|
|
594
|
+
action: "accounts.notification_batch.finalize",
|
|
595
|
+
outcome: "allowed",
|
|
596
|
+
actor: params.actor,
|
|
597
|
+
target: { type: "notification_batch", id: params.id },
|
|
598
|
+
metadata: { targetCount: candidates.length, deliverableCount },
|
|
599
|
+
},
|
|
600
|
+
tx,
|
|
601
|
+
);
|
|
579
602
|
return ok(mapBatch(updated[0]!));
|
|
580
603
|
});
|
|
581
604
|
|
|
@@ -584,28 +607,45 @@ export const finalize = async (params: {
|
|
|
584
607
|
return ok({ batch: result.data, jobId });
|
|
585
608
|
};
|
|
586
609
|
|
|
587
|
-
export const retryFailed = async (params: {
|
|
610
|
+
export const retryFailed = async (params: {
|
|
611
|
+
id: string;
|
|
612
|
+
actor: AuditActor & { userId: string };
|
|
613
|
+
}): Promise<Result<{ batch: NotificationBatch; jobId: string }>> => {
|
|
588
614
|
const batch = await get(params.id);
|
|
589
615
|
if (!batch) return fail(err.notFound("Notification batch not found"));
|
|
590
616
|
if (batch.status === "draft" || batch.status === "cancelled") {
|
|
591
617
|
return fail(err.conflict("Only finalized notification batches can retry recipients"));
|
|
592
618
|
}
|
|
593
619
|
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
620
|
+
const result = await sql.begin(async (tx) => {
|
|
621
|
+
const updated = await tx<BatchRow[]>`
|
|
622
|
+
UPDATE notifications.batch_recipients
|
|
623
|
+
SET status = 'pending', error = NULL, notification_id = NULL, sent_at = NULL, updated_at = now()
|
|
624
|
+
WHERE batch_id = ${params.id}::uuid AND status = 'error' AND recipient IS NOT NULL
|
|
625
|
+
RETURNING user_id
|
|
626
|
+
`;
|
|
627
|
+
if (updated.length === 0) {
|
|
628
|
+
return fail(err.conflict("No failed deliverable recipients can be retried"));
|
|
629
|
+
}
|
|
603
630
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
631
|
+
await tx`
|
|
632
|
+
UPDATE notifications.batches
|
|
633
|
+
SET status = 'ready', completed_at = NULL, last_error = NULL
|
|
634
|
+
WHERE id = ${params.id}::uuid AND status IN ('completed_with_errors', 'failed', 'running', 'ready', 'completed')
|
|
635
|
+
`;
|
|
636
|
+
await audit.record(
|
|
637
|
+
{
|
|
638
|
+
action: "accounts.notification_batch.retry_failed",
|
|
639
|
+
outcome: "allowed",
|
|
640
|
+
actor: params.actor,
|
|
641
|
+
target: { type: "notification_batch", id: params.id },
|
|
642
|
+
metadata: { recipientCount: updated.length },
|
|
643
|
+
},
|
|
644
|
+
tx,
|
|
645
|
+
);
|
|
646
|
+
return ok(null);
|
|
647
|
+
});
|
|
648
|
+
if (!result.ok) return result;
|
|
609
649
|
const refreshed = await refreshBatchCounters(params.id);
|
|
610
650
|
// A completing worker may already have sampled zero remaining recipients.
|
|
611
651
|
// Explicit retries need their own wakeup even while that worker holds its key.
|
|
@@ -615,6 +655,7 @@ export const retryFailed = async (params: { id: string }): Promise<Result<{ batc
|
|
|
615
655
|
|
|
616
656
|
export const retryRecipient = async (params: {
|
|
617
657
|
id: string;
|
|
658
|
+
actor: AuditActor & { userId: string };
|
|
618
659
|
userId: string;
|
|
619
660
|
}): Promise<Result<{ batch: NotificationBatch; jobId: string }>> => {
|
|
620
661
|
const batch = await get(params.id);
|
|
@@ -623,30 +664,44 @@ export const retryRecipient = async (params: {
|
|
|
623
664
|
return fail(err.conflict("Only finalized notification batches can retry recipients"));
|
|
624
665
|
}
|
|
625
666
|
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (!updated[0]) {
|
|
636
|
-
const existing = await sql<BatchRow[]>`
|
|
637
|
-
SELECT status, recipient
|
|
638
|
-
FROM notifications.batch_recipients
|
|
639
|
-
WHERE batch_id = ${params.id}::uuid AND user_id = ${params.userId}::uuid
|
|
667
|
+
const result = await sql.begin(async (tx) => {
|
|
668
|
+
const updated = await tx<BatchRow[]>`
|
|
669
|
+
UPDATE notifications.batch_recipients
|
|
670
|
+
SET status = 'pending', error = NULL, notification_id = NULL, sent_at = NULL, updated_at = now()
|
|
671
|
+
WHERE batch_id = ${params.id}::uuid
|
|
672
|
+
AND user_id = ${params.userId}::uuid
|
|
673
|
+
AND status = 'error'
|
|
674
|
+
AND recipient IS NOT NULL
|
|
675
|
+
RETURNING *
|
|
640
676
|
`;
|
|
641
|
-
if (!
|
|
642
|
-
|
|
643
|
-
|
|
677
|
+
if (!updated[0]) {
|
|
678
|
+
const existing = await tx<BatchRow[]>`
|
|
679
|
+
SELECT status, recipient
|
|
680
|
+
FROM notifications.batch_recipients
|
|
681
|
+
WHERE batch_id = ${params.id}::uuid AND user_id = ${params.userId}::uuid
|
|
682
|
+
`;
|
|
683
|
+
if (!existing[0]) return fail(err.notFound("Notification recipient not found"));
|
|
684
|
+
return fail(err.conflict("Only failed deliverable recipients can be retried"));
|
|
685
|
+
}
|
|
644
686
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
687
|
+
await tx`
|
|
688
|
+
UPDATE notifications.batches
|
|
689
|
+
SET status = 'ready', completed_at = NULL, last_error = NULL
|
|
690
|
+
WHERE id = ${params.id}::uuid AND status IN ('completed_with_errors', 'failed', 'running', 'ready', 'completed')
|
|
691
|
+
`;
|
|
692
|
+
await audit.record(
|
|
693
|
+
{
|
|
694
|
+
action: "accounts.notification_batch.retry_recipient",
|
|
695
|
+
outcome: "allowed",
|
|
696
|
+
actor: params.actor,
|
|
697
|
+
target: { type: "notification_batch", id: params.id },
|
|
698
|
+
metadata: { recipientUserId: params.userId },
|
|
699
|
+
},
|
|
700
|
+
tx,
|
|
701
|
+
);
|
|
702
|
+
return ok(null);
|
|
703
|
+
});
|
|
704
|
+
if (!result.ok) return result;
|
|
650
705
|
const refreshed = await refreshBatchCounters(params.id);
|
|
651
706
|
const { jobId } = await batchJob().submit({
|
|
652
707
|
key: `${params.id}:retry:${crypto.randomUUID()}`,
|
|
@@ -655,17 +710,29 @@ export const retryRecipient = async (params: {
|
|
|
655
710
|
return ok({ batch: refreshed ?? batch, jobId });
|
|
656
711
|
};
|
|
657
712
|
|
|
658
|
-
export const removeDraft = async (params: { id: string }): Promise<Result<{ id: string }>> => {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
713
|
+
export const removeDraft = async (params: { id: string; actor: AuditActor & { userId: string } }): Promise<Result<{ id: string }>> => {
|
|
714
|
+
return sql.begin(async (tx) => {
|
|
715
|
+
const rows = await tx<BatchRow[]>`
|
|
716
|
+
DELETE FROM notifications.batches
|
|
717
|
+
WHERE id = ${params.id}::uuid AND status = 'draft'
|
|
718
|
+
RETURNING id
|
|
719
|
+
`;
|
|
720
|
+
if (rows[0]) {
|
|
721
|
+
await audit.record(
|
|
722
|
+
{
|
|
723
|
+
action: "accounts.notification_batch.delete",
|
|
724
|
+
outcome: "allowed",
|
|
725
|
+
actor: params.actor,
|
|
726
|
+
target: { type: "notification_batch", id: params.id },
|
|
727
|
+
},
|
|
728
|
+
tx,
|
|
729
|
+
);
|
|
730
|
+
return ok({ id: params.id });
|
|
731
|
+
}
|
|
732
|
+
const existing = await tx`SELECT id FROM notifications.batches WHERE id = ${params.id}::uuid`;
|
|
733
|
+
if (!existing[0]) return fail(err.notFound("Notification batch not found"));
|
|
734
|
+
return fail(err.conflict("Only draft notification batches can be deleted"));
|
|
735
|
+
});
|
|
669
736
|
};
|
|
670
737
|
|
|
671
738
|
export const start = async (): Promise<void> => {
|
|
@@ -719,4 +786,5 @@ export const __notificationBatchTest = {
|
|
|
719
786
|
selectionHash,
|
|
720
787
|
recipientHash,
|
|
721
788
|
resolveCandidates,
|
|
789
|
+
processBatchChunk,
|
|
722
790
|
};
|
|
@@ -152,6 +152,18 @@ const browserDriver = {
|
|
|
152
152
|
},
|
|
153
153
|
deliver: async (value: unknown) => {
|
|
154
154
|
const payload: BrowserDeliveryPayload = BrowserDeliveryPayloadSchema.parse(value);
|
|
155
|
+
// A queued payload can outlive logout, disable, or rebinding this browser to another user.
|
|
156
|
+
const [endpoint] = await sql<{ id: string }[]>`
|
|
157
|
+
SELECT id FROM notifications.endpoints
|
|
158
|
+
WHERE id = ${payload.endpointId}::uuid AND channel = 'browser'
|
|
159
|
+
AND verified_at IS NOT NULL AND disabled_at IS NULL
|
|
160
|
+
`;
|
|
161
|
+
if (!endpoint) {
|
|
162
|
+
throw Object.assign(new Error("Browser notification endpoint is no longer registered"), {
|
|
163
|
+
code: "endpoint_gone",
|
|
164
|
+
retryable: false,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
155
167
|
await ensureWebPushConfigured();
|
|
156
168
|
try {
|
|
157
169
|
await sendPinnedWebPush(
|
|
@@ -6,7 +6,6 @@ import { logger } from "../logging";
|
|
|
6
6
|
import { escapeLikePattern } from "../postgres";
|
|
7
7
|
import { sendEmail } from "./email";
|
|
8
8
|
import "./browser";
|
|
9
|
-
import { notificationLive } from "./live";
|
|
10
9
|
import { notificationObservability } from "./observability";
|
|
11
10
|
import { sendTypedNotification, type TypedNotificationSendResult } from "./platform";
|
|
12
11
|
import { userNotifications } from "./user";
|
|
@@ -610,7 +609,6 @@ export const notifications = {
|
|
|
610
609
|
sendAllPendingSystem,
|
|
611
610
|
getStatusSummary,
|
|
612
611
|
getSearchSummary,
|
|
613
|
-
live: notificationLive,
|
|
614
612
|
observability: notificationObservability,
|
|
615
613
|
user: userNotifications,
|
|
616
614
|
};
|
|
@@ -15,7 +15,6 @@ import { encryptSecret } from "../secrets";
|
|
|
15
15
|
import { ensureNotificationDefinition } from "./catalog";
|
|
16
16
|
import { getNotificationChannel, type NotificationDestination, type ResolvedNotificationRecipient } from "./channels";
|
|
17
17
|
import { processNotificationDelivery } from "./dispatcher";
|
|
18
|
-
import { notificationLive } from "./live";
|
|
19
18
|
import { enqueueNotificationDeliveries, enqueueNotificationDelivery } from "./runtime";
|
|
20
19
|
|
|
21
20
|
export type TypedNotificationDeliveryStatus = "deferred" | "pending" | "sending" | "delivered" | "suppressed" | "failed";
|
|
@@ -406,9 +405,6 @@ export const sendTypedNotification = async <
|
|
|
406
405
|
});
|
|
407
406
|
|
|
408
407
|
if (persisted.prepared) {
|
|
409
|
-
if (resolved.recipient.userId && (requiredChannelSet.has("browser") || preferred.includes("browser"))) {
|
|
410
|
-
await notificationLive.publish({ userId: resolved.recipient.userId, eventId: identity.id, presentation });
|
|
411
|
-
}
|
|
412
408
|
for (const deliveryId of persisted.requiredIds) {
|
|
413
409
|
const result = await processNotificationDelivery(deliveryId);
|
|
414
410
|
if (result.status === "retry") await enqueueNotificationDelivery(deliveryId, result.retryAfterMs);
|
|
@@ -6,7 +6,7 @@ import { isAccountExpired } from "./account-model";
|
|
|
6
6
|
import { IDENTITY_JWKS_MAX_AGE_SECONDS } from "./identity/constants";
|
|
7
7
|
import { getIdentityRuntimeConfig } from "./identity/runtime-config";
|
|
8
8
|
import type { ServiceAccount } from "./service-accounts";
|
|
9
|
-
import { buildProjectedUser, userProjectionSql } from "./session/user";
|
|
9
|
+
import { buildProjectedUser, userProjectionJoin, userProjectionSql } from "./session/user";
|
|
10
10
|
|
|
11
11
|
type ServiceAccountActorRow = Record<string, unknown> & {
|
|
12
12
|
service_account_id: string;
|
|
@@ -116,7 +116,7 @@ export const resolveOAuthTokenActor = async (
|
|
|
116
116
|
FROM oauth.clients c
|
|
117
117
|
JOIN auth.service_accounts sa ON sa.id = ${serviceAccountId}::uuid AND sa.status = 'active'
|
|
118
118
|
LEFT JOIN auth.users u ON u.id = sa.delegated_user_id
|
|
119
|
-
|
|
119
|
+
${userProjectionJoin()}
|
|
120
120
|
WHERE c.client_id = ${clientId}
|
|
121
121
|
`;
|
|
122
122
|
const row = rows[0];
|
|
@@ -139,7 +139,7 @@ export const resolveOAuthTokenActor = async (
|
|
|
139
139
|
SELECT ${userProjectionSql(groupsAdmin)}
|
|
140
140
|
FROM oauth.clients c
|
|
141
141
|
JOIN auth.users u ON ${predicate}
|
|
142
|
-
|
|
142
|
+
${userProjectionJoin()}
|
|
143
143
|
WHERE c.client_id = ${clientId}
|
|
144
144
|
`;
|
|
145
145
|
const user = rows[0] ? buildProjectedUser(rows[0]) : null;
|
|
@@ -34,6 +34,18 @@ export type RenderHtmlToPdfInput = {
|
|
|
34
34
|
headerHtml?: string | null;
|
|
35
35
|
footerHtml?: string | null;
|
|
36
36
|
filename?: string;
|
|
37
|
+
assets?: Array<{ name: string; data: Blob }>;
|
|
38
|
+
page?: {
|
|
39
|
+
format?: "A4" | "A3" | "A5" | "Letter" | "Legal";
|
|
40
|
+
landscape?: boolean;
|
|
41
|
+
margin?: { top?: number; right?: number; bottom?: number; left?: number };
|
|
42
|
+
};
|
|
43
|
+
tagged?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type AttachPdfFilesInput = {
|
|
47
|
+
document: Blob;
|
|
48
|
+
attachments: Array<{ name: string; data: Blob; relationship?: "Source" | "Data" | "Alternative" | "Supplement" | "Unspecified" }>;
|
|
37
49
|
};
|
|
38
50
|
|
|
39
51
|
export type RenderFacturXHtmlToPdfInput = RenderHtmlToPdfInput & {
|
|
@@ -59,6 +71,7 @@ export type GotenbergFetch = (input: string | URL | Request, init?: RequestInit)
|
|
|
59
71
|
|
|
60
72
|
export type RenderHtmlToPdfOptions = {
|
|
61
73
|
fetch?: GotenbergFetch;
|
|
74
|
+
signal?: AbortSignal;
|
|
62
75
|
};
|
|
63
76
|
|
|
64
77
|
const DEFAULT_PDF_CONTENT_TYPE = "application/pdf";
|
|
@@ -105,111 +118,154 @@ const sanitizeFetchError = (error: unknown): GotenbergRenderError => {
|
|
|
105
118
|
return new GotenbergRenderError("request_failed", "Gotenberg request failed.");
|
|
106
119
|
};
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (htmlBytes > config.maxHtmlBytes) {
|
|
115
|
-
throw new GotenbergRenderError("html_too_large", `HTML input is too large (${htmlBytes} bytes, limit ${config.maxHtmlBytes} bytes).`);
|
|
116
|
-
}
|
|
121
|
+
const dimensions = { A4: [210, 297], A3: [297, 420], A5: [148, 210], Letter: [215.9, 279.4], Legal: [215.9, 355.6] } as const;
|
|
122
|
+
const fileName = (name: string) => {
|
|
123
|
+
if (!name || name.length > 180 || /[\\/\x00-\x1f]/.test(name) || name === "." || name === "..")
|
|
124
|
+
throw new GotenbergRenderError("bad_input", "Use a plain filename without directories.");
|
|
125
|
+
return name;
|
|
126
|
+
};
|
|
117
127
|
|
|
118
|
-
|
|
128
|
+
function htmlForm(input: RenderHtmlToPdfInput, config: GotenbergConfig): FormData {
|
|
129
|
+
const total =
|
|
130
|
+
[input.html, input.headerHtml ?? "", input.footerHtml ?? ""].reduce((sum, value) => sum + byteLength(value), 0) +
|
|
131
|
+
(input.assets ?? []).reduce((sum, asset) => sum + asset.data.size, 0);
|
|
132
|
+
if (total > config.maxHtmlBytes) throw new GotenbergRenderError("html_too_large", "HTML and assets exceed the configured input budget.");
|
|
119
133
|
const form = new FormData();
|
|
120
134
|
form.append("files", new Blob([input.html], { type: "text/html" }), "index.html");
|
|
121
135
|
if (input.headerHtml?.trim()) form.append("files", new Blob([input.headerHtml], { type: "text/html" }), "header.html");
|
|
122
136
|
if (input.footerHtml?.trim()) form.append("files", new Blob([input.footerHtml], { type: "text/html" }), "footer.html");
|
|
123
|
-
|
|
137
|
+
const names = new Set(["index.html", "header.html", "footer.html", "factur-x.xml"]);
|
|
138
|
+
for (const asset of input.assets ?? []) {
|
|
139
|
+
const name = fileName(asset.name);
|
|
140
|
+
if (names.has(name.toLowerCase()))
|
|
141
|
+
throw new GotenbergRenderError("bad_input", "Asset filenames must be unique and cannot replace HTML documents.");
|
|
142
|
+
names.add(name.toLowerCase());
|
|
143
|
+
form.append("files", asset.data, name);
|
|
144
|
+
}
|
|
145
|
+
// Preserve the existing CSS page-size contract unless a caller supplies page options.
|
|
146
|
+
form.append("preferCssPageSize", String(!input.page));
|
|
124
147
|
form.append("printBackground", "true");
|
|
148
|
+
if (input.tagged !== undefined) form.append("generateTaggedPdf", String(input.tagged));
|
|
149
|
+
if (input.page) {
|
|
150
|
+
const [width, height] = dimensions[input.page.format ?? "A4"];
|
|
151
|
+
form.append("paperWidth", String(width / 25.4));
|
|
152
|
+
form.append("paperHeight", String(height / 25.4));
|
|
153
|
+
form.append("landscape", String(input.page.landscape ?? false));
|
|
154
|
+
for (const side of ["top", "right", "bottom", "left"] as const) {
|
|
155
|
+
const value = input.page.margin?.[side] ?? 15;
|
|
156
|
+
if (!Number.isFinite(value) || value < 0)
|
|
157
|
+
throw new GotenbergRenderError("bad_input", "Page margins must be nonnegative millimeters.");
|
|
158
|
+
form.append(`margin${side[0]!.toUpperCase()}${side.slice(1)}`, String(value / 25.4));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return form;
|
|
162
|
+
}
|
|
125
163
|
|
|
164
|
+
/** Bounded transport shared by HTML conversion and attachment embedding. */
|
|
165
|
+
async function requestPdf(
|
|
166
|
+
route: string,
|
|
167
|
+
form: FormData,
|
|
168
|
+
config: GotenbergConfig,
|
|
169
|
+
options: RenderHtmlToPdfOptions,
|
|
170
|
+
): Promise<RenderHtmlToPdfResult> {
|
|
126
171
|
const headers = new Headers();
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let response: Response;
|
|
172
|
+
const auth = basicAuthHeader(config);
|
|
173
|
+
if (auth) headers.set("Authorization", auth);
|
|
174
|
+
const signal = options.signal ? AbortSignal.any([options.signal, abortSignal(config.timeoutMs)]) : abortSignal(config.timeoutMs);
|
|
175
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
132
176
|
try {
|
|
133
|
-
response = await
|
|
177
|
+
const response = await (options.fetch ?? fetch)(`${normalizeBaseUrl(config.url)}${route}`, {
|
|
134
178
|
method: "POST",
|
|
135
179
|
headers,
|
|
136
180
|
body: form,
|
|
137
|
-
signal
|
|
181
|
+
signal,
|
|
138
182
|
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
await response.body?.cancel();
|
|
185
|
+
throw new GotenbergRenderError("bad_response", `Gotenberg returned HTTP ${response.status}.`, response.status);
|
|
186
|
+
}
|
|
187
|
+
const contentType = response.headers.get("content-type") || DEFAULT_PDF_CONTENT_TYPE;
|
|
188
|
+
if (contentType.split(";")[0] !== DEFAULT_PDF_CONTENT_TYPE) {
|
|
189
|
+
await response.body?.cancel();
|
|
190
|
+
throw new GotenbergRenderError("bad_response", "Gotenberg did not return a PDF.");
|
|
191
|
+
}
|
|
192
|
+
if (!response.body) throw new GotenbergRenderError("bad_response", "Gotenberg returned an empty response.");
|
|
193
|
+
reader = response.body.getReader();
|
|
194
|
+
const chunks: Uint8Array[] = [];
|
|
195
|
+
let size = 0;
|
|
196
|
+
while (true) {
|
|
197
|
+
signal.throwIfAborted();
|
|
198
|
+
const item = await reader.read();
|
|
199
|
+
if (item.done) break;
|
|
200
|
+
size += item.value.byteLength;
|
|
201
|
+
if (size > config.maxPdfBytes) throw new GotenbergRenderError("pdf_too_large", "PDF output exceeds the configured budget.");
|
|
202
|
+
chunks.push(item.value);
|
|
203
|
+
}
|
|
204
|
+
const pdf = new Uint8Array(size);
|
|
205
|
+
let offset = 0;
|
|
206
|
+
for (const chunk of chunks) {
|
|
207
|
+
pdf.set(chunk, offset);
|
|
208
|
+
offset += chunk.byteLength;
|
|
209
|
+
}
|
|
210
|
+
return { pdf, contentType };
|
|
139
211
|
} catch (error) {
|
|
212
|
+
await reader?.cancel().catch(() => {});
|
|
140
213
|
throw sanitizeFetchError(error);
|
|
214
|
+
} finally {
|
|
215
|
+
reader?.releaseLock();
|
|
141
216
|
}
|
|
217
|
+
}
|
|
142
218
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (buffer.byteLength > config.maxPdfBytes) {
|
|
149
|
-
throw new GotenbergRenderError(
|
|
150
|
-
"pdf_too_large",
|
|
151
|
-
`PDF output is too large (${buffer.byteLength} bytes, limit ${config.maxPdfBytes} bytes).`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
pdf: new Uint8Array(buffer),
|
|
157
|
-
contentType: response.headers.get("content-type") || DEFAULT_PDF_CONTENT_TYPE,
|
|
158
|
-
};
|
|
159
|
-
};
|
|
219
|
+
export const renderHtmlToPdfWithConfig = async (
|
|
220
|
+
input: RenderHtmlToPdfInput,
|
|
221
|
+
config: GotenbergConfig,
|
|
222
|
+
options: RenderHtmlToPdfOptions = {},
|
|
223
|
+
): Promise<RenderHtmlToPdfResult> => requestPdf("/forms/chromium/convert/html", htmlForm(input, config), config, options);
|
|
160
224
|
|
|
161
225
|
export const renderFacturXHtmlToPdfWithConfig = async (
|
|
162
226
|
input: RenderFacturXHtmlToPdfInput,
|
|
163
227
|
config: GotenbergConfig,
|
|
164
228
|
options: RenderHtmlToPdfOptions = {},
|
|
165
229
|
): Promise<RenderHtmlToPdfResult> => {
|
|
166
|
-
const htmlBytes = byteLength(input.html);
|
|
167
230
|
const xmlBytes = byteLength(input.xml);
|
|
168
|
-
if (
|
|
169
|
-
throw new GotenbergRenderError("html_too_large", `HTML input is too large (${htmlBytes} bytes, limit ${config.maxHtmlBytes} bytes).`);
|
|
170
|
-
}
|
|
171
|
-
if (xmlBytes === 0 || xmlBytes > config.maxHtmlBytes) {
|
|
231
|
+
if (!xmlBytes || xmlBytes > config.maxHtmlBytes)
|
|
172
232
|
throw new GotenbergRenderError(
|
|
173
|
-
xmlBytes
|
|
174
|
-
|
|
175
|
-
? "Factur-X XML must not be empty."
|
|
176
|
-
: `Factur-X XML input is too large (${xmlBytes} bytes, limit ${config.maxHtmlBytes} bytes).`,
|
|
233
|
+
xmlBytes ? "html_too_large" : "bad_input",
|
|
234
|
+
"Invoice XML is empty or exceeds the configured input budget.",
|
|
177
235
|
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const baseUrl = normalizeBaseUrl(config.url);
|
|
181
|
-
const form = new FormData();
|
|
182
|
-
form.append("files", new Blob([input.html], { type: "text/html" }), "index.html");
|
|
183
|
-
if (input.headerHtml?.trim()) form.append("files", new Blob([input.headerHtml], { type: "text/html" }), "header.html");
|
|
184
|
-
if (input.footerHtml?.trim()) form.append("files", new Blob([input.footerHtml], { type: "text/html" }), "footer.html");
|
|
236
|
+
const form = htmlForm(input, { ...config, maxHtmlBytes: config.maxHtmlBytes - xmlBytes });
|
|
185
237
|
form.append("facturxXml", new Blob([input.xml], { type: "application/xml" }), "factur-x.xml");
|
|
186
238
|
form.append("facturxConformanceLevel", input.conformanceLevel ?? "EN 16931");
|
|
187
239
|
form.append("facturxDocumentType", input.documentType ?? "INVOICE");
|
|
188
240
|
form.append("facturxVersion", input.facturXVersion ?? "1.0");
|
|
189
241
|
form.append("pdfa", "PDF/A-3b");
|
|
190
|
-
|
|
191
|
-
|
|
242
|
+
return requestPdf("/forms/chromium/convert/html", form, config, options);
|
|
243
|
+
};
|
|
192
244
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
245
|
+
export const attachPdfFilesWithConfig = async (
|
|
246
|
+
input: AttachPdfFilesInput,
|
|
247
|
+
config: GotenbergConfig,
|
|
248
|
+
options: RenderHtmlToPdfOptions = {},
|
|
249
|
+
): Promise<RenderHtmlToPdfResult> => {
|
|
250
|
+
if (!input.attachments.length) throw new GotenbergRenderError("bad_input", "At least one attachment is required.");
|
|
251
|
+
if (input.document.size + input.attachments.reduce((sum, file) => sum + file.data.size, 0) > config.maxPdfBytes)
|
|
252
|
+
throw new GotenbergRenderError("pdf_too_large", "PDF and attachments exceed the configured input budget.");
|
|
253
|
+
const form = new FormData();
|
|
254
|
+
form.append("files", input.document, "document.pdf");
|
|
255
|
+
const metadata: Record<string, { mimeType: string; relationship: string }> = Object.create(null);
|
|
256
|
+
const names = new Set<string>();
|
|
257
|
+
for (const attachment of input.attachments) {
|
|
258
|
+
const name = fileName(attachment.name);
|
|
259
|
+
if (names.has(name.toLowerCase())) throw new GotenbergRenderError("bad_input", "Attachment filenames must be unique.");
|
|
260
|
+
names.add(name.toLowerCase());
|
|
261
|
+
form.append("embeds", attachment.data, name);
|
|
262
|
+
metadata[name] = {
|
|
263
|
+
mimeType: attachment.data.type || "application/octet-stream",
|
|
264
|
+
relationship: attachment.relationship ?? "Unspecified",
|
|
265
|
+
};
|
|
211
266
|
}
|
|
212
|
-
|
|
267
|
+
form.append("embedsMetadata", JSON.stringify(metadata));
|
|
268
|
+
return requestPdf("/forms/pdfengines/embed", form, config, options);
|
|
213
269
|
};
|
|
214
270
|
|
|
215
271
|
export const mergePdfsWithConfig = async (
|
|
@@ -288,3 +344,6 @@ export const testGotenberg = async (options: RenderHtmlToPdfOptions = {}): Promi
|
|
|
288
344
|
const result = await renderHtmlToPdf({ html: TEST_HTML, filename: "index.html" }, options);
|
|
289
345
|
return { bytes: result.pdf.byteLength, contentType: result.contentType };
|
|
290
346
|
};
|
|
347
|
+
|
|
348
|
+
export const attachPdfFiles = async (input: AttachPdfFilesInput, options: RenderHtmlToPdfOptions = {}): Promise<RenderHtmlToPdfResult> =>
|
|
349
|
+
attachPdfFilesWithConfig(input, await getGotenbergConfig(), options);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type {
|
|
2
|
+
AttachPdfFilesInput,
|
|
2
3
|
GotenbergConfig,
|
|
3
4
|
GotenbergRenderErrorCode,
|
|
4
5
|
MergePdfsInput,
|
|
@@ -8,6 +9,8 @@ export type {
|
|
|
8
9
|
RenderHtmlToPdfResult,
|
|
9
10
|
} from "./gotenberg";
|
|
10
11
|
export {
|
|
12
|
+
attachPdfFiles,
|
|
13
|
+
attachPdfFilesWithConfig,
|
|
11
14
|
GotenbergRenderError,
|
|
12
15
|
getGotenbergConfig,
|
|
13
16
|
mergePdfs,
|
|
@@ -18,14 +21,6 @@ export {
|
|
|
18
21
|
renderHtmlToPdfWithConfig,
|
|
19
22
|
testGotenberg,
|
|
20
23
|
} from "./gotenberg";
|
|
21
|
-
export type {
|
|
22
|
-
RenderTemplatePdfPreviewInput,
|
|
23
|
-
RenderTemplatePdfPreviewOptions,
|
|
24
|
-
TemplatePdfPreviewError,
|
|
25
|
-
TemplatePdfPreviewPhase,
|
|
26
|
-
TemplatePdfPreviewResult,
|
|
27
|
-
} from "./template-preview";
|
|
28
|
-
export { renderTemplatePdfPreview } from "./template-preview";
|
|
29
24
|
export type {
|
|
30
25
|
MarkdownPdfErrorCode,
|
|
31
26
|
MarkdownPdfTemplateId,
|
|
@@ -41,3 +36,11 @@ export {
|
|
|
41
36
|
renderMarkdownToPdf,
|
|
42
37
|
renderMarkdownToPdfWithConfig,
|
|
43
38
|
} from "./markdown";
|
|
39
|
+
export type {
|
|
40
|
+
RenderTemplatePdfPreviewInput,
|
|
41
|
+
RenderTemplatePdfPreviewOptions,
|
|
42
|
+
TemplatePdfPreviewError,
|
|
43
|
+
TemplatePdfPreviewPhase,
|
|
44
|
+
TemplatePdfPreviewResult,
|
|
45
|
+
} from "./template-preview";
|
|
46
|
+
export { renderTemplatePdfPreview } from "./template-preview";
|