@storyshelf/core 0.3.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -31
- package/dist/adapter/capture-queue.d.mts +29 -1
- package/dist/adapter/capture-runner.d.mts +18 -2
- package/dist/adapter/capture-runner.mjs +12 -1
- package/dist/adapter/capture-runner.mjs.map +1 -0
- package/dist/adapter/database.d.mts +67 -17
- package/dist/adapter/database.mjs +22 -55
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/email-sender.d.mts +27 -0
- package/dist/adapter/email-sender.mjs +4 -0
- package/dist/adapter/metadata.d.mts +21 -15
- package/dist/adapter/notifier.d.mts +7 -0
- package/dist/adapter/notifier.mjs +6 -0
- package/dist/adapter/setup.d.mts +61 -0
- package/dist/adapter/setup.mjs +29 -0
- package/dist/adapter/setup.mjs.map +1 -0
- package/dist/adapter/webhook-events.d.mts +29 -0
- package/dist/adapter/webhook-events.mjs +83 -0
- package/dist/adapter/webhook-events.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +26 -0
- package/dist/adapters/notifier/events.mjs +66 -0
- package/dist/adapters/notifier/events.mjs.map +1 -0
- package/dist/adapters/notifier/format.d.mts +14 -0
- package/dist/adapters/notifier/format.mjs +96 -0
- package/dist/adapters/notifier/format.mjs.map +1 -0
- package/dist/adapters/notifier/provider.d.mts +32 -0
- package/dist/adapters/notifier/types.d.mts +56 -0
- package/dist/adapters/setup-runner.d.mts +23 -0
- package/dist/adapters/setup-runner.mjs +149 -0
- package/dist/adapters/setup-runner.mjs.map +1 -0
- package/dist/adapters/validate.d.mts +30 -0
- package/dist/adapters/validate.mjs +313 -0
- package/dist/adapters/validate.mjs.map +1 -0
- package/dist/auth.d.mts +94 -0
- package/dist/auth.mjs +4 -0
- package/dist/brand-packs.d.mts +16 -0
- package/dist/brand-packs.mjs +130 -0
- package/dist/brand-packs.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +51 -1
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +16 -0
- package/dist/capture/affected.mjs +64 -0
- package/dist/capture/affected.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +30 -0
- package/dist/capture/diff-record.mjs.map +1 -0
- package/dist/capture/dispatch.d.mts +36 -0
- package/dist/capture/dispatch.mjs +88 -31
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/guards.mjs +31 -0
- package/dist/capture/guards.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +41 -6
- package/dist/capture/orchestrator.mjs +151 -28
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +30 -16
- package/dist/capture/pipeline.mjs +143 -39
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/recovery.d.mts +21 -0
- package/dist/capture/recovery.mjs +83 -0
- package/dist/capture/recovery.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +35 -0
- package/dist/capture/rediff.mjs +77 -0
- package/dist/capture/rediff.mjs.map +1 -0
- package/dist/capture/sizing.d.mts +58 -0
- package/dist/capture/sizing.mjs +119 -0
- package/dist/capture/sizing.mjs.map +1 -0
- package/dist/capture/skip-checks.mjs +3 -3
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +38 -0
- package/dist/capture/statics.mjs +38 -4
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/status-fanout.mjs +7 -2
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +31 -2
- package/dist/capture/storybook.mjs +77 -2
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture.d.mts +10 -4
- package/dist/capture.mjs +7 -2
- package/dist/config.d.mts +121 -4
- package/dist/config.mjs +55 -8
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.mts +5 -56
- package/dist/index.mjs +4 -284
- package/dist/logger.d.mts +26 -17
- package/dist/logger.mjs +10 -5
- package/dist/logger.mjs.map +1 -1
- package/dist/models/baseline-status.d.mts +29 -0
- package/dist/models/baseline-status.mjs +26 -0
- package/dist/models/baseline-status.mjs.map +1 -0
- package/dist/models/baseline.d.mts +33 -0
- package/dist/models/baseline.mjs +35 -13
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.d.mts +87 -0
- package/dist/models/build.mjs +83 -35
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +40 -0
- package/dist/models/capture-attempt.mjs +85 -0
- package/dist/models/capture-attempt.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +21 -0
- package/dist/models/capture-log.mjs +44 -0
- package/dist/models/capture-log.mjs.map +1 -0
- package/dist/models/comment.d.mts +29 -0
- package/dist/models/comment.mjs +11 -8
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/content-ref.d.mts +21 -0
- package/dist/models/content-ref.mjs +49 -0
- package/dist/models/content-ref.mjs.map +1 -0
- package/dist/models/credentials.d.mts +36 -0
- package/dist/models/credentials.mjs +103 -0
- package/dist/models/credentials.mjs.map +1 -0
- package/dist/models/label.d.mts +58 -0
- package/dist/models/label.mjs +32 -27
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +34 -0
- package/dist/models/member.mjs +20 -11
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +52 -0
- package/dist/models/notification-channel.mjs +93 -0
- package/dist/models/notification-channel.mjs.map +1 -0
- package/dist/models/notification-subscription.d.mts +44 -0
- package/dist/models/notification-subscription.mjs +89 -0
- package/dist/models/notification-subscription.mjs.map +1 -0
- package/dist/models/project-group-mapping.d.mts +30 -0
- package/dist/models/project-group-mapping.mjs +46 -0
- package/dist/models/project-group-mapping.mjs.map +1 -0
- package/dist/models/project.d.mts +54 -0
- package/dist/models/project.mjs +15 -11
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.d.mts +58 -0
- package/dist/models/snapshot.mjs +13 -7
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.d.mts +38 -0
- package/dist/models/status-config.mjs +12 -11
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.d.mts +44 -0
- package/dist/models/token.mjs +18 -15
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/user.d.mts +42 -0
- package/dist/models/user.mjs +65 -0
- package/dist/models/user.mjs.map +1 -0
- package/dist/models/webhook.d.mts +50 -0
- package/dist/models/webhook.mjs +12 -10
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +22 -0
- package/dist/models.mjs +23 -0
- package/dist/paths.mjs +29 -0
- package/dist/paths.mjs.map +1 -1
- package/dist/retention.d.mts +45 -0
- package/dist/retention.mjs +145 -0
- package/dist/retention.mjs.map +1 -0
- package/dist/schema/baseline.d.mts +1 -0
- package/dist/schema/build.d.mts +8 -0
- package/dist/schema/capture-attempt.d.mts +38 -0
- package/dist/schema/comment.d.mts +1 -1
- package/dist/schema/content-ref.d.mts +13 -0
- package/dist/schema/member.d.mts +2 -0
- package/dist/schema/notification.d.mts +35 -0
- package/dist/schema/project-group-mapping.d.mts +15 -0
- package/dist/schema/project.d.mts +4 -0
- package/dist/schema/snapshot.d.mts +7 -0
- package/dist/schema/token.d.mts +2 -0
- package/dist/schema/user.d.mts +11 -1
- package/dist/schema.d.mts +9 -46
- package/dist/schema.mjs +1 -55
- package/dist/test-helpers/create-project.d.mts +15 -0
- package/dist/test-helpers/create-project.mjs +28 -0
- package/dist/test-helpers/create-project.mjs.map +1 -0
- package/dist/test-helpers/database-contract.d.mts +13 -0
- package/dist/test-helpers/database-contract.mjs +58 -0
- package/dist/test-helpers/database-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-adapters.d.mts +14 -0
- package/dist/test-helpers/fake-adapters.mjs +20 -0
- package/dist/test-helpers/fake-adapters.mjs.map +1 -0
- package/dist/test-helpers/fake-database.d.mts +11 -0
- package/dist/test-helpers/fake-database.mjs +104 -0
- package/dist/test-helpers/fake-database.mjs.map +1 -0
- package/dist/test-helpers/fake-storage.d.mts +14 -0
- package/dist/test-helpers/fake-storage.mjs +49 -0
- package/dist/test-helpers/fake-storage.mjs.map +1 -0
- package/dist/test-helpers/fake-tables.mjs +276 -0
- package/dist/test-helpers/fake-tables.mjs.map +1 -0
- package/dist/test-helpers/git-contract.d.mts +11 -0
- package/dist/test-helpers/git-contract.mjs +34 -0
- package/dist/test-helpers/git-contract.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +11 -0
- package/dist/test-helpers/notifier-contract.mjs +34 -0
- package/dist/test-helpers/notifier-contract.mjs.map +1 -0
- package/dist/test-helpers/queue-contract.d.mts +11 -0
- package/dist/test-helpers/queue-contract.mjs +64 -0
- package/dist/test-helpers/queue-contract.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.d.mts +11 -0
- package/dist/test-helpers/runner-contract.mjs +32 -0
- package/dist/test-helpers/runner-contract.mjs.map +1 -0
- package/dist/test-helpers/sql-chunks.mjs +138 -0
- package/dist/test-helpers/sql-chunks.mjs.map +1 -0
- package/dist/test-helpers/storage-contract.d.mts +11 -0
- package/dist/test-helpers/storage-contract.mjs +87 -0
- package/dist/test-helpers/storage-contract.mjs.map +1 -0
- package/dist/test-helpers.d.mts +13 -0
- package/dist/test-helpers.mjs +14 -0
- package/dist/tracing.mjs +63 -0
- package/dist/tracing.mjs.map +1 -0
- package/dist/types.d.mts +38 -5
- package/dist/types.mjs +18 -5
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +12 -1
- package/dist/urls.mjs +22 -3
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/encrypt.d.mts +48 -0
- package/dist/utils/encrypt.mjs +67 -13
- package/dist/utils/encrypt.mjs.map +1 -1
- package/dist/utils/hash.d.mts +17 -0
- package/dist/utils/hash.mjs +8 -2
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/http.d.mts +58 -0
- package/dist/utils/http.mjs +147 -0
- package/dist/utils/http.mjs.map +1 -0
- package/dist/utils/mime.d.mts +9 -0
- package/dist/utils/mime.mjs +33 -0
- package/dist/utils/mime.mjs.map +1 -0
- package/dist/utils/path-security.d.mts +12 -0
- package/dist/utils/path-security.mjs +24 -0
- package/dist/utils/path-security.mjs.map +1 -0
- package/dist/utils/redact.d.mts +17 -0
- package/dist/utils/redact.mjs +42 -0
- package/dist/utils/redact.mjs.map +1 -0
- package/dist/utils/timing.d.mts +19 -0
- package/dist/utils/timing.mjs +46 -0
- package/dist/utils/timing.mjs.map +1 -0
- package/dist/utils/ulid.d.mts +10 -0
- package/dist/utils/ulid.mjs +8 -8
- package/dist/utils/ulid.mjs.map +1 -1
- package/dist/utils.d.mts +12 -0
- package/dist/utils.mjs +13 -0
- package/package.json +69 -28
- package/dist/adapter/auth.d.mts +0 -50
- package/dist/adapter/auth.mjs +0 -10
- package/dist/adapter/auth.mjs.map +0 -1
- package/dist/adapter/init.d.mts +0 -58
- package/dist/adapter/init.mjs +0 -93
- package/dist/adapter/init.mjs.map +0 -1
- package/dist/adapters/webhook-events.mjs +0 -55
- package/dist/adapters/webhook-events.mjs.map +0 -1
- package/dist/ddl.d.mts +0 -8
- package/dist/ddl.mjs +0 -158
- package/dist/ddl.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/middleware/auth-gate.mjs +0 -36
- package/dist/middleware/auth-gate.mjs.map +0 -1
- package/dist/middleware/csrf.mjs +0 -40
- package/dist/middleware/csrf.mjs.map +0 -1
- package/dist/middleware/rate-limit.mjs +0 -39
- package/dist/middleware/rate-limit.mjs.map +0 -1
- package/dist/middleware/request-log.mjs +0 -28
- package/dist/middleware/request-log.mjs.map +0 -1
- package/dist/middleware/store-scope.mjs +0 -29
- package/dist/middleware/store-scope.mjs.map +0 -1
- package/dist/pages/build-detail.mjs +0 -339
- package/dist/pages/build-detail.mjs.map +0 -1
- package/dist/pages/build-diff-comments.mjs +0 -113
- package/dist/pages/build-diff-comments.mjs.map +0 -1
- package/dist/pages/build-diff-header.mjs +0 -163
- package/dist/pages/build-diff-header.mjs.map +0 -1
- package/dist/pages/build-diff-nav.mjs +0 -66
- package/dist/pages/build-diff-nav.mjs.map +0 -1
- package/dist/pages/build-diff-viewer.mjs +0 -197
- package/dist/pages/build-diff-viewer.mjs.map +0 -1
- package/dist/pages/build-diff.mjs +0 -90
- package/dist/pages/build-diff.mjs.map +0 -1
- package/dist/pages/compute-jobs.mjs +0 -198
- package/dist/pages/compute-jobs.mjs.map +0 -1
- package/dist/pages/label-detail.mjs +0 -228
- package/dist/pages/label-detail.mjs.map +0 -1
- package/dist/pages/login.mjs +0 -78
- package/dist/pages/login.mjs.map +0 -1
- package/dist/pages/project-builds.mjs +0 -231
- package/dist/pages/project-builds.mjs.map +0 -1
- package/dist/pages/project-create.mjs +0 -169
- package/dist/pages/project-create.mjs.map +0 -1
- package/dist/pages/project-settings.mjs +0 -102
- package/dist/pages/project-settings.mjs.map +0 -1
- package/dist/pages/projects.mjs +0 -184
- package/dist/pages/projects.mjs.map +0 -1
- package/dist/pages/root.mjs +0 -94
- package/dist/pages/root.mjs.map +0 -1
- package/dist/pages/settings-general.mjs +0 -192
- package/dist/pages/settings-general.mjs.map +0 -1
- package/dist/pages/settings-labels.mjs +0 -150
- package/dist/pages/settings-labels.mjs.map +0 -1
- package/dist/pages/settings-members.mjs +0 -125
- package/dist/pages/settings-members.mjs.map +0 -1
- package/dist/pages/settings-status.mjs +0 -152
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/pages/settings-tests.mjs +0 -149
- package/dist/pages/settings-tests.mjs.map +0 -1
- package/dist/pages/settings-tokens.mjs +0 -96
- package/dist/pages/settings-tokens.mjs.map +0 -1
- package/dist/pages/settings-webhooks.mjs +0 -150
- package/dist/pages/settings-webhooks.mjs.map +0 -1
- package/dist/pages/storybook.mjs +0 -96
- package/dist/pages/storybook.mjs.map +0 -1
- package/dist/retention/purge.mjs +0 -67
- package/dist/retention/purge.mjs.map +0 -1
- package/dist/routers/admin.mjs +0 -42
- package/dist/routers/admin.mjs.map +0 -1
- package/dist/routers/assets.mjs +0 -25
- package/dist/routers/assets.mjs.map +0 -1
- package/dist/routers/auth.mjs +0 -78
- package/dist/routers/auth.mjs.map +0 -1
- package/dist/routers/builds.handlers.mjs +0 -184
- package/dist/routers/builds.handlers.mjs.map +0 -1
- package/dist/routers/builds.mjs +0 -185
- package/dist/routers/builds.mjs.map +0 -1
- package/dist/routers/comments.mjs +0 -90
- package/dist/routers/comments.mjs.map +0 -1
- package/dist/routers/health.mjs +0 -137
- package/dist/routers/health.mjs.map +0 -1
- package/dist/routers/helpers.mjs +0 -82
- package/dist/routers/helpers.mjs.map +0 -1
- package/dist/routers/htmx.mjs +0 -20
- package/dist/routers/htmx.mjs.map +0 -1
- package/dist/routers/labels.mjs +0 -110
- package/dist/routers/labels.mjs.map +0 -1
- package/dist/routers/media.mjs +0 -55
- package/dist/routers/media.mjs.map +0 -1
- package/dist/routers/members.mjs +0 -102
- package/dist/routers/members.mjs.map +0 -1
- package/dist/routers/projects.mjs +0 -122
- package/dist/routers/projects.mjs.map +0 -1
- package/dist/routers/schemas.mjs +0 -224
- package/dist/routers/schemas.mjs.map +0 -1
- package/dist/routers/settings-general.mjs +0 -95
- package/dist/routers/settings-general.mjs.map +0 -1
- package/dist/routers/settings-labels.mjs +0 -49
- package/dist/routers/settings-labels.mjs.map +0 -1
- package/dist/routers/settings-members.mjs +0 -30
- package/dist/routers/settings-members.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -73
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/settings-tokens.mjs +0 -40
- package/dist/routers/settings-tokens.mjs.map +0 -1
- package/dist/routers/settings-webhooks.mjs +0 -63
- package/dist/routers/settings-webhooks.mjs.map +0 -1
- package/dist/routers/settings.handlers.mjs +0 -93
- package/dist/routers/settings.handlers.mjs.map +0 -1
- package/dist/routers/settings.mjs +0 -23
- package/dist/routers/settings.mjs.map +0 -1
- package/dist/routers/snapshots.mjs +0 -144
- package/dist/routers/snapshots.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -101
- package/dist/routers/status-configs.mjs.map +0 -1
- package/dist/routers/storybook.mjs +0 -120
- package/dist/routers/storybook.mjs.map +0 -1
- package/dist/routers/tokens.mjs +0 -81
- package/dist/routers/tokens.mjs.map +0 -1
- package/dist/routers/ui.mjs +0 -165
- package/dist/routers/ui.mjs.map +0 -1
- package/dist/routers/webhooks.mjs +0 -90
- package/dist/routers/webhooks.mjs.map +0 -1
- package/dist/schema/baseline.mjs +0 -22
- package/dist/schema/baseline.mjs.map +0 -1
- package/dist/schema/build.mjs +0 -29
- package/dist/schema/build.mjs.map +0 -1
- package/dist/schema/comment.mjs +0 -26
- package/dist/schema/comment.mjs.map +0 -1
- package/dist/schema/label.mjs +0 -30
- package/dist/schema/label.mjs.map +0 -1
- package/dist/schema/member.mjs +0 -19
- package/dist/schema/member.mjs.map +0 -1
- package/dist/schema/project.mjs +0 -25
- package/dist/schema/project.mjs.map +0 -1
- package/dist/schema/snapshot.mjs +0 -34
- package/dist/schema/snapshot.mjs.map +0 -1
- package/dist/schema/status-config.mjs +0 -20
- package/dist/schema/status-config.mjs.map +0 -1
- package/dist/schema/token.mjs +0 -19
- package/dist/schema/token.mjs.map +0 -1
- package/dist/schema/user.mjs +0 -19
- package/dist/schema/user.mjs.map +0 -1
- package/dist/schema/webhook.mjs +0 -20
- package/dist/schema/webhook.mjs.map +0 -1
- package/dist/schema.mjs.map +0 -1
- package/dist/store.mjs +0 -20
- package/dist/store.mjs.map +0 -1
- package/dist/ui/document.mjs +0 -348
- package/dist/ui/document.mjs.map +0 -1
- package/dist/ui/feedback.mjs +0 -24
- package/dist/ui/feedback.mjs.map +0 -1
- package/dist/ui/forms.mjs +0 -117
- package/dist/ui/forms.mjs.map +0 -1
- package/dist/ui/styles.mjs +0 -174
- package/dist/ui/styles.mjs.map +0 -1
- package/dist/ui/theme.mjs +0 -44
- package/dist/ui/theme.mjs.map +0 -1
|
@@ -11,7 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
* `category` tells a reader which slot the adapter fills without knowing
|
|
12
12
|
* which adapter instance supplied the metadata.
|
|
13
13
|
*/
|
|
14
|
-
type AdapterCategory = "database" | "storage" | "
|
|
14
|
+
type AdapterCategory = "database" | "storage" | "capture-runner" | "capture-queue" | "git-host" | "notifier";
|
|
15
15
|
/** Common adapter identity — every adapter instance exposes this. */
|
|
16
16
|
interface AdapterMetadata {
|
|
17
17
|
/** Human label for UI/logs. */
|
|
@@ -31,39 +31,39 @@ interface GitAdapterMetadata extends AdapterMetadata {
|
|
|
31
31
|
readonly logo?: string;
|
|
32
32
|
readonly schema: z.ZodType;
|
|
33
33
|
}
|
|
34
|
-
/** Context passed to an adapter's `
|
|
35
|
-
interface
|
|
34
|
+
/** Context passed to an adapter's `setup` on server startup. */
|
|
35
|
+
interface AdapterSetupContext {
|
|
36
36
|
/** Resolved shelf configuration (scratchDir, secret, …). Never log the secret. */
|
|
37
37
|
readonly config: ShelfConfig;
|
|
38
|
-
/** Scoped logger for
|
|
38
|
+
/** Scoped logger for setup-time diagnostics. */
|
|
39
39
|
readonly logger: Logger;
|
|
40
40
|
}
|
|
41
41
|
/** Result of an adapter's ongoing health probe. */
|
|
42
42
|
interface AdapterHealth {
|
|
43
43
|
/** Whether the adapter is currently usable. */
|
|
44
44
|
readonly ok: boolean;
|
|
45
|
-
/** Probe latency in milliseconds, when measured. */
|
|
46
|
-
readonly latencyMs?: number;
|
|
47
45
|
/** Short human-readable detail (must not contain secrets). */
|
|
48
46
|
readonly detail?: string;
|
|
49
47
|
}
|
|
50
48
|
/**
|
|
51
|
-
* Optional lifecycle every adapter can expose.
|
|
49
|
+
* Optional lifecycle every adapter can expose — all or nothing.
|
|
52
50
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
51
|
+
* An adapter either omits `lifecycle` entirely or implements all three
|
|
52
|
+
* hooks; partial implementations are not allowed. Grouped in a sub-object
|
|
53
|
+
* (rather than flattened onto each adapter) so future hooks land in one
|
|
54
|
+
* place. All hooks must be idempotent — the router may run them more than
|
|
55
|
+
* once per process.
|
|
56
56
|
*/
|
|
57
57
|
interface AdapterLifecycle {
|
|
58
58
|
/**
|
|
59
59
|
* One-shot setup: migrations, directory creation, credential validation.
|
|
60
60
|
* Throw when something required is missing.
|
|
61
61
|
*/
|
|
62
|
-
|
|
63
|
-
/** Teardown:
|
|
64
|
-
|
|
62
|
+
setup: (ctx: AdapterSetupContext) => Promise<void>;
|
|
63
|
+
/** Teardown: destroy clients and handles. Must tolerate repeated calls. */
|
|
64
|
+
teardown: () => Promise<void>;
|
|
65
65
|
/** Ongoing liveness probe (cheap: SELECT 1, access(), head-bucket). */
|
|
66
|
-
health
|
|
66
|
+
health: () => Promise<AdapterHealth>;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Shared adapter base.
|
|
@@ -77,7 +77,13 @@ interface Adapter<Extra extends object = Record<string, unknown>> {
|
|
|
77
77
|
readonly metadata: AdapterMetadata & Extra;
|
|
78
78
|
/** Optional lifecycle hooks. */
|
|
79
79
|
readonly lifecycle?: AdapterLifecycle;
|
|
80
|
+
/**
|
|
81
|
+
* Bind the host-owned runtime logger. Called by the app/worker when
|
|
82
|
+
* present; factories must never create their own loggers — they only
|
|
83
|
+
* store what they are given (explicit `options.logger` wins over bound).
|
|
84
|
+
*/
|
|
85
|
+
setLogger?(logger: Logger): void;
|
|
80
86
|
}
|
|
81
87
|
//#endregion
|
|
82
|
-
export { Adapter, AdapterCategory, AdapterHealth,
|
|
88
|
+
export { Adapter, AdapterCategory, AdapterHealth, AdapterLifecycle, AdapterMetadata, AdapterSetupContext, GitAdapterMetadata };
|
|
83
89
|
//# sourceMappingURL=metadata.d.mts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { ChannelToggles, FormattedNotification, NotificationBrand, NotificationChannel, NotificationEvent, SystemEventName, SystemNotification } from "../adapters/notifier/types.mjs";
|
|
4
|
+
import { NotifierAdapter, NotifierMetadata, NotifierProvider } from "../adapters/notifier/provider.mjs";
|
|
5
|
+
import { emitNotifications, emitSystemNotification } from "../adapters/notifier/events.mjs";
|
|
6
|
+
import { formatNotification, formatSystemNotification } from "../adapters/notifier/format.mjs";
|
|
7
|
+
export { type ChannelToggles, type FormattedNotification, type NotificationBrand, type NotificationChannel, type NotificationEvent, type NotifierAdapter, type NotifierMetadata, type NotifierProvider, type SystemEventName, type SystemNotification, emitNotifications, emitSystemNotification, formatNotification, formatSystemNotification };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { formatNotification, formatSystemNotification } from "../adapters/notifier/format.mjs";
|
|
5
|
+
import { emitNotifications, emitSystemNotification } from "../adapters/notifier/events.mjs";
|
|
6
|
+
export { emitNotifications, emitSystemNotification, formatNotification, formatSystemNotification };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { CaptureRunner } from "./capture-runner.mjs";
|
|
4
|
+
import { DatabaseAdapter } from "./database.mjs";
|
|
5
|
+
import { EmailSender } from "./email-sender.mjs";
|
|
6
|
+
import { GitHostProvider } from "./git-host.mjs";
|
|
7
|
+
import { NotifierProvider } from "../adapters/notifier/provider.mjs";
|
|
8
|
+
import { StorageAdapter } from "./storage.mjs";
|
|
9
|
+
import { AdapterSetupContext } from "./metadata.mjs";
|
|
10
|
+
import { CaptureQueue } from "./capture-queue.mjs";
|
|
11
|
+
import { bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns } from "../adapters/setup-runner.mjs";
|
|
12
|
+
import { validateAdapter, validateAdapterSources, validateAuth, validateBootAssembly } from "../adapters/validate.mjs";
|
|
13
|
+
//#region src/adapters/setup.d.ts
|
|
14
|
+
/** Adapters that can take part in lifecycle runs. */
|
|
15
|
+
interface AdapterSetupSources {
|
|
16
|
+
database: DatabaseAdapter;
|
|
17
|
+
storage: StorageAdapter;
|
|
18
|
+
captureRunner?: CaptureRunner;
|
|
19
|
+
captureQueue?: CaptureQueue;
|
|
20
|
+
gitHosts?: GitHostProvider[];
|
|
21
|
+
notifiers?: NotifierProvider[];
|
|
22
|
+
emailSender?: EmailSender;
|
|
23
|
+
}
|
|
24
|
+
/** One collected hook bound to its adapter identity. */
|
|
25
|
+
interface HookEntry {
|
|
26
|
+
readonly category: string;
|
|
27
|
+
readonly kind: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly run: (ctx: AdapterSetupContext) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/** One failed hook, with its adapter identity attached. */
|
|
32
|
+
interface AdapterSetupFailure {
|
|
33
|
+
readonly category: string;
|
|
34
|
+
readonly kind: string;
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly error: string;
|
|
37
|
+
}
|
|
38
|
+
/** Settled outcome of a lifecycle run (never rejects — use `ok`). */
|
|
39
|
+
interface AdapterSetupResult {
|
|
40
|
+
readonly ok: boolean;
|
|
41
|
+
readonly failures: AdapterSetupFailure[];
|
|
42
|
+
}
|
|
43
|
+
/** Lifecycle phase a run covers. */
|
|
44
|
+
type LifecyclePhase = "setup" | "teardown";
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when an `app.lifecycle.setup()` or `teardown()` run finishes with failures.
|
|
47
|
+
*
|
|
48
|
+
* Inspect `phase` to know which hook failed and `failures` for per-adapter
|
|
49
|
+
* diagnostics. The router converts this into a 500 with structured details
|
|
50
|
+
* during health checks.
|
|
51
|
+
*/
|
|
52
|
+
declare class AdapterLifecycleError extends Error {
|
|
53
|
+
/** Which lifecycle phase failed (`setup` or `teardown`). */
|
|
54
|
+
readonly phase: LifecyclePhase;
|
|
55
|
+
/** One entry per adapter hook that failed. */
|
|
56
|
+
readonly failures: AdapterSetupFailure[];
|
|
57
|
+
constructor(phase: LifecyclePhase, failures: AdapterSetupFailure[]);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { AdapterLifecycleError, AdapterSetupFailure, AdapterSetupResult, AdapterSetupSources, HookEntry, LifecyclePhase, bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns, validateAdapter, validateAdapterSources, validateAuth, validateBootAssembly };
|
|
61
|
+
//# sourceMappingURL=setup.d.mts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns } from "../adapters/setup-runner.mjs";
|
|
5
|
+
import { validateAdapter, validateAdapterSources, validateAuth, validateBootAssembly } from "../adapters/validate.mjs";
|
|
6
|
+
//#region src/adapters/setup.ts
|
|
7
|
+
/**
|
|
8
|
+
* Thrown when an `app.lifecycle.setup()` or `teardown()` run finishes with failures.
|
|
9
|
+
*
|
|
10
|
+
* Inspect `phase` to know which hook failed and `failures` for per-adapter
|
|
11
|
+
* diagnostics. The router converts this into a 500 with structured details
|
|
12
|
+
* during health checks.
|
|
13
|
+
*/
|
|
14
|
+
var AdapterLifecycleError = class extends Error {
|
|
15
|
+
/** Which lifecycle phase failed (`setup` or `teardown`). */
|
|
16
|
+
phase;
|
|
17
|
+
/** One entry per adapter hook that failed. */
|
|
18
|
+
failures;
|
|
19
|
+
constructor(phase, failures) {
|
|
20
|
+
super(`Adapter ${phase} failed: ${failures.map((f) => `${f.category}/${f.kind}`).join(", ")}`);
|
|
21
|
+
this.name = "AdapterLifecycleError";
|
|
22
|
+
this.phase = phase;
|
|
23
|
+
this.failures = failures;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AdapterLifecycleError, bindAdapterLoggers, collectSetups, collectTeardowns, runAdapterSetups, runAdapterTeardowns, validateAdapter, validateAdapterSources, validateAuth, validateBootAssembly };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=setup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.mjs","names":[],"sources":["../../src/adapters/setup.ts"],"sourcesContent":["/**\n * Adapter lifecycle contract: hooks every adapter can expose, plus the\n * settled-result and error types. The runner (`setup-runner.ts`) collects\n * and executes the hooks; `createShelfApp` kicks `setup` eagerly and\n * gates the first request on the shared result. All hooks must be\n * idempotent.\n */\nimport type { CaptureQueue } from \"./capture-queue.ts\";\nimport type { CaptureRunner } from \"./capture-runner.ts\";\nimport type { DatabaseAdapter } from \"./database.ts\";\nimport type { EmailSender } from \"./email-sender.ts\";\nimport type { GitHostProvider } from \"./git-host/index.ts\";\nimport type { AdapterSetupContext } from \"./metadata.ts\";\nimport type { NotifierProvider } from \"./notifier/provider.ts\";\nimport type { StorageAdapter } from \"./storage.ts\";\n\n/** Adapters that can take part in lifecycle runs. */\nexport interface AdapterSetupSources {\n database: DatabaseAdapter;\n storage: StorageAdapter;\n captureRunner?: CaptureRunner;\n captureQueue?: CaptureQueue;\n gitHosts?: GitHostProvider[];\n notifiers?: NotifierProvider[];\n emailSender?: EmailSender;\n}\n\n/** One collected hook bound to its adapter identity. */\nexport interface HookEntry {\n readonly category: string;\n readonly kind: string;\n readonly name: string;\n readonly run: (ctx: AdapterSetupContext) => Promise<void>;\n}\n\n/** One failed hook, with its adapter identity attached. */\nexport interface AdapterSetupFailure {\n readonly category: string;\n readonly kind: string;\n readonly name: string;\n readonly error: string;\n}\n\n/** Settled outcome of a lifecycle run (never rejects — use `ok`). */\nexport interface AdapterSetupResult {\n readonly ok: boolean;\n readonly failures: AdapterSetupFailure[];\n}\n\n/** Lifecycle phase a run covers. */\nexport type LifecyclePhase = \"setup\" | \"teardown\";\n\n/**\n * Thrown when an `app.lifecycle.setup()` or `teardown()` run finishes with failures.\n *\n * Inspect `phase` to know which hook failed and `failures` for per-adapter\n * diagnostics. The router converts this into a 500 with structured details\n * during health checks.\n */\nexport class AdapterLifecycleError extends Error {\n /** Which lifecycle phase failed (`setup` or `teardown`). */\n readonly phase: LifecyclePhase;\n /** One entry per adapter hook that failed. */\n readonly failures: AdapterSetupFailure[];\n constructor(phase: LifecyclePhase, failures: AdapterSetupFailure[]) {\n super(`Adapter ${phase} failed: ${failures.map((f) => `${f.category}/${f.kind}`).join(\", \")}`);\n this.name = \"AdapterLifecycleError\";\n this.phase = phase;\n this.failures = failures;\n }\n}\n\n/** Collect every `setup` hook from the configured adapters. */\nexport {\n bindAdapterLoggers,\n collectSetups,\n collectTeardowns,\n runAdapterSetups,\n runAdapterTeardowns,\n} from \"./setup-runner.ts\";\n\n/** Boot-time shape validation for third-party adapters. */\nexport {\n validateAdapter,\n validateAdapterSources,\n validateAuth,\n validateBootAssembly,\n} from \"./validate.ts\";\n"],"mappings":";;;;;;;;;;;;;AA2DA,IAAa,wBAAb,cAA2C,MAAM;;CAE/C;;CAEA;CACA,YAAY,OAAuB,UAAiC;EAClE,MAAM,WAAW,MAAM,WAAW,SAAS,KAAK,MAAM,GAAG,EAAE,SAAS,GAAG,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI,GAAG;EAC7F,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,WAAW;CAClB;AACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "./database.mjs";
|
|
4
|
+
import { SecretInput } from "../utils/encrypt.mjs";
|
|
5
|
+
import { WebhookTables } from "../models/webhook.mjs";
|
|
6
|
+
import { Logger } from "pino";
|
|
7
|
+
//#region src/adapters/webhook-events.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Deliver an event to every subscribed webhook of a project.
|
|
10
|
+
*
|
|
11
|
+
* @param db - Database adapter for loading subscriptions.
|
|
12
|
+
* @param tables - Webhook table handles.
|
|
13
|
+
* @param projectId - Project whose webhooks receive the event.
|
|
14
|
+
* @param event - Event name (e.g. "baseline:created").
|
|
15
|
+
* @param data - Event payload.
|
|
16
|
+
* @param secret - Server secret for decrypting webhook secrets (in memory only).
|
|
17
|
+
* @param logger - Optional logger for per-delivery outcomes.
|
|
18
|
+
*/
|
|
19
|
+
declare function emitWebhookEvent(db: DatabaseAdapter, tables: WebhookTables, projectId: string, event: string, data: Record<string, unknown>, secret: SecretInput, logger?: Logger): Promise<void>;
|
|
20
|
+
/** Outbound webhook payload delivered to subscribers. */
|
|
21
|
+
interface WebhookEvent {
|
|
22
|
+
event: string;
|
|
23
|
+
projectId: string;
|
|
24
|
+
data: Record<string, unknown>;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { WebhookEvent, emitWebhookEvent };
|
|
29
|
+
//# sourceMappingURL=webhook-events.d.mts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { WebhookModel } from "../models/webhook.mjs";
|
|
5
|
+
import { hmacSha256 } from "../utils/hash.mjs";
|
|
6
|
+
//#region src/adapters/webhook-events.ts
|
|
7
|
+
/** Per-delivery budget: single attempt, no retries (events already fan out). */
|
|
8
|
+
const WEBHOOK_TIMEOUT_MS = 1e4;
|
|
9
|
+
/**
|
|
10
|
+
* Deliver an event to every subscribed webhook of a project.
|
|
11
|
+
*
|
|
12
|
+
* @param db - Database adapter for loading subscriptions.
|
|
13
|
+
* @param tables - Webhook table handles.
|
|
14
|
+
* @param projectId - Project whose webhooks receive the event.
|
|
15
|
+
* @param event - Event name (e.g. "baseline:created").
|
|
16
|
+
* @param data - Event payload.
|
|
17
|
+
* @param secret - Server secret for decrypting webhook secrets (in memory only).
|
|
18
|
+
* @param logger - Optional logger for per-delivery outcomes.
|
|
19
|
+
*/
|
|
20
|
+
async function emitWebhookEvent(db, tables, projectId, event, data, secret, logger) {
|
|
21
|
+
const webhookModel = new WebhookModel(db, tables, secret);
|
|
22
|
+
const webhooks = await webhookModel.list(projectId);
|
|
23
|
+
const eventPayload = {
|
|
24
|
+
event,
|
|
25
|
+
projectId,
|
|
26
|
+
data,
|
|
27
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
28
|
+
};
|
|
29
|
+
await Promise.allSettled(webhooks.map(async (webhook) => {
|
|
30
|
+
const events = WebhookModel.eventsOf(webhook);
|
|
31
|
+
if (events.length > 0 && !events.includes(event)) return;
|
|
32
|
+
await deliverToWebhook(webhookModel, webhook, eventPayload, logger);
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
/** Decrypt, send, and log one webhook delivery (failures stay non-fatal). */
|
|
36
|
+
async function deliverToWebhook(webhookModel, webhook, eventPayload, logger) {
|
|
37
|
+
let plaintext;
|
|
38
|
+
try {
|
|
39
|
+
plaintext = webhookModel.decryptSecret(webhook);
|
|
40
|
+
} catch {
|
|
41
|
+
logger?.warn({ webhookId: webhook.id }, "skipping webhook with undecryptable secret");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await sendWebhook(webhook.url, plaintext, eventPayload);
|
|
46
|
+
logger?.info({
|
|
47
|
+
webhookId: webhook.id,
|
|
48
|
+
host: hostOf(webhook.url)
|
|
49
|
+
}, "webhook delivered");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger?.warn({
|
|
52
|
+
webhookId: webhook.id,
|
|
53
|
+
host: hostOf(webhook.url),
|
|
54
|
+
err: error
|
|
55
|
+
}, "webhook delivery failed");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Hostname of a webhook URL for logs (never the full URL — may carry secrets). */
|
|
59
|
+
function hostOf(url) {
|
|
60
|
+
try {
|
|
61
|
+
return new URL(url).host;
|
|
62
|
+
} catch {
|
|
63
|
+
return "[invalid-url]";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function sendWebhook(url, secret, event) {
|
|
67
|
+
const body = JSON.stringify(event);
|
|
68
|
+
const signature = hmacSha256(secret, body);
|
|
69
|
+
await fetch(url, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"X-StoryShelf-Event": event.event,
|
|
74
|
+
"X-StoryShelf-Signature": signature
|
|
75
|
+
},
|
|
76
|
+
body,
|
|
77
|
+
signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { emitWebhookEvent };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=webhook-events.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-events.mjs","names":[],"sources":["../../src/adapters/webhook-events.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport { WebhookModel, type WebhookTables } from \"../models/webhook.ts\";\nimport type { Webhook } from \"../schema/webhook.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\nimport { hmacSha256 } from \"../utils/hash.ts\";\nimport type { DatabaseAdapter } from \"./database.ts\";\n\n/** Per-delivery budget: single attempt, no retries (events already fan out). */\nconst WEBHOOK_TIMEOUT_MS = 10_000;\n\n/**\n * Deliver an event to every subscribed webhook of a project.\n *\n * @param db - Database adapter for loading subscriptions.\n * @param tables - Webhook table handles.\n * @param projectId - Project whose webhooks receive the event.\n * @param event - Event name (e.g. \"baseline:created\").\n * @param data - Event payload.\n * @param secret - Server secret for decrypting webhook secrets (in memory only).\n * @param logger - Optional logger for per-delivery outcomes.\n */\nexport async function emitWebhookEvent(\n db: DatabaseAdapter,\n tables: WebhookTables,\n projectId: string,\n event: string,\n data: Record<string, unknown>,\n secret: SecretInput,\n logger?: Logger,\n): Promise<void> {\n const webhookModel = new WebhookModel(db, tables, secret);\n const webhooks = await webhookModel.list(projectId);\n const eventPayload: WebhookEvent = {\n event,\n projectId,\n data,\n timestamp: new Date().toISOString(),\n };\n\n await Promise.allSettled(\n webhooks.map(async (webhook) => {\n const events = WebhookModel.eventsOf(webhook);\n if (events.length > 0 && !events.includes(event)) {\n return;\n }\n await deliverToWebhook(webhookModel, webhook, eventPayload, logger);\n }),\n );\n}\n\n/** Decrypt, send, and log one webhook delivery (failures stay non-fatal). */\nasync function deliverToWebhook(\n webhookModel: WebhookModel,\n webhook: Webhook,\n eventPayload: WebhookEvent,\n logger: Logger | undefined,\n): Promise<void> {\n let plaintext: string;\n try {\n plaintext = webhookModel.decryptSecret(webhook);\n } catch {\n // Undecryptable secret (e.g. rotated server SECRET) — skip, don't leak\n logger?.warn({ webhookId: webhook.id }, \"skipping webhook with undecryptable secret\");\n return;\n }\n try {\n await sendWebhook(webhook.url, plaintext, eventPayload);\n logger?.info({ webhookId: webhook.id, host: hostOf(webhook.url) }, \"webhook delivered\");\n } catch (error) {\n // Webhook delivery failures are non-fatal\n logger?.warn(\n { webhookId: webhook.id, host: hostOf(webhook.url), err: error },\n \"webhook delivery failed\",\n );\n }\n}\n\n/** Outbound webhook payload delivered to subscribers. */\nexport interface WebhookEvent {\n event: string;\n projectId: string;\n data: Record<string, unknown>;\n timestamp: string;\n}\n\n/** Hostname of a webhook URL for logs (never the full URL — may carry secrets). */\nfunction hostOf(url: string): string {\n try {\n return new URL(url).host;\n } catch {\n return \"[invalid-url]\";\n }\n}\n\nasync function sendWebhook(url: string, secret: string, event: WebhookEvent): Promise<void> {\n const body = JSON.stringify(event);\n const signature = hmacSha256(secret, body);\n await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-StoryShelf-Event\": event.event,\n \"X-StoryShelf-Signature\": signature,\n },\n body,\n signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS),\n });\n}\n"],"mappings":";;;;;;;AAQA,MAAM,qBAAqB;;;;;;;;;;;;AAa3B,eAAsB,iBACpB,IACA,QACA,WACA,OACA,MACA,QACA,QACe;CACf,MAAM,eAAe,IAAI,aAAa,IAAI,QAAQ,MAAM;CACxD,MAAM,WAAW,MAAM,aAAa,KAAK,SAAS;CAClD,MAAM,eAA6B;EACjC;EACA;EACA;EACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CACpC;CAEA,MAAM,QAAQ,WACZ,SAAS,IAAI,OAAO,YAAY;EAC9B,MAAM,SAAS,aAAa,SAAS,OAAO;EAC5C,IAAI,OAAO,SAAS,KAAK,CAAC,OAAO,SAAS,KAAK,GAC7C;EAEF,MAAM,iBAAiB,cAAc,SAAS,cAAc,MAAM;CACpE,CAAC,CACH;AACF;;AAGA,eAAe,iBACb,cACA,SACA,cACA,QACe;CACf,IAAI;CACJ,IAAI;EACF,YAAY,aAAa,cAAc,OAAO;CAChD,QAAQ;EAEN,QAAQ,KAAK,EAAE,WAAW,QAAQ,GAAG,GAAG,4CAA4C;EACpF;CACF;CACA,IAAI;EACF,MAAM,YAAY,QAAQ,KAAK,WAAW,YAAY;EACtD,QAAQ,KAAK;GAAE,WAAW,QAAQ;GAAI,MAAM,OAAO,QAAQ,GAAG;EAAE,GAAG,mBAAmB;CACxF,SAAS,OAAO;EAEd,QAAQ,KACN;GAAE,WAAW,QAAQ;GAAI,MAAM,OAAO,QAAQ,GAAG;GAAG,KAAK;EAAM,GAC/D,yBACF;CACF;AACF;;AAWA,SAAS,OAAO,KAAqB;CACnC,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACtB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,YAAY,KAAa,QAAgB,OAAoC;CAC1F,MAAM,OAAO,KAAK,UAAU,KAAK;CACjC,MAAM,YAAY,WAAW,QAAQ,IAAI;CACzC,MAAM,MAAM,KAAK;EACf,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,sBAAsB,MAAM;GAC5B,0BAA0B;EAC5B;EACA;EACA,QAAQ,YAAY,QAAQ,kBAAkB;CAChD,CAAC;AACH"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { NotificationBrand, NotificationChannel, NotificationEvent, SystemNotification } from "./types.mjs";
|
|
4
|
+
import { NotifierProvider } from "./provider.mjs";
|
|
5
|
+
import { Logger } from "../../logger.mjs";
|
|
6
|
+
//#region src/adapters/notifier/events.d.ts
|
|
7
|
+
/** Options for project-event fan-out (inline best-effort, non-fatal). */
|
|
8
|
+
interface EmitNotificationsOptions {
|
|
9
|
+
notifiers?: NotifierProvider[];
|
|
10
|
+
channels?: NotificationChannel[];
|
|
11
|
+
brand?: NotificationBrand;
|
|
12
|
+
logger?: Logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Deliver a project event to every subscribed channel.
|
|
16
|
+
* Inline best-effort: `Promise.allSettled`, failures logged and swallowed.
|
|
17
|
+
*/
|
|
18
|
+
declare function emitNotifications(event: NotificationEvent, options?: EmitNotificationsOptions): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Deliver a site-wide admin alert to project-less channels
|
|
21
|
+
* (e.g. `sys:user-created`, `sys:capture-failed`, `sys:purge-completed`).
|
|
22
|
+
*/
|
|
23
|
+
declare function emitSystemNotification(event: SystemNotification, options?: EmitNotificationsOptions): Promise<void>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { EmitNotificationsOptions, emitNotifications, emitSystemNotification };
|
|
26
|
+
//# sourceMappingURL=events.d.mts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { formatNotification, formatSystemNotification } from "./format.mjs";
|
|
5
|
+
//#region src/adapters/notifier/events.ts
|
|
6
|
+
/** Check an event against a channel's allowlist (empty = all). */
|
|
7
|
+
function subscribed(channel, event) {
|
|
8
|
+
if (!channel.enabled) return false;
|
|
9
|
+
return channel.events.length === 0 || channel.events.includes(event);
|
|
10
|
+
}
|
|
11
|
+
/** Resolve a provider for a channel kind, or undefined when unwired. */
|
|
12
|
+
function providerFor(notifiers, kind) {
|
|
13
|
+
return notifiers?.find((candidate) => candidate.metadata.kind === kind);
|
|
14
|
+
}
|
|
15
|
+
/** Send one formatted event through a channel's provider (throws on failure). */
|
|
16
|
+
async function deliver(notifiers, channel, formatted) {
|
|
17
|
+
const provider = providerFor(notifiers, channel.provider);
|
|
18
|
+
if (!provider) throw new Error(`notifier missing: ${channel.provider}`);
|
|
19
|
+
await provider.create({
|
|
20
|
+
config: channel.config,
|
|
21
|
+
secret: channel.secret
|
|
22
|
+
}).send({
|
|
23
|
+
channel,
|
|
24
|
+
formatted
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Deliver a project event to every subscribed channel.
|
|
29
|
+
* Inline best-effort: `Promise.allSettled`, failures logged and swallowed.
|
|
30
|
+
*/
|
|
31
|
+
async function emitNotifications(event, options = {}) {
|
|
32
|
+
const targets = (options.channels ?? []).filter((channel) => subscribed(channel, event.event));
|
|
33
|
+
if (targets.length === 0) return;
|
|
34
|
+
await Promise.allSettled(targets.map(async (channel) => {
|
|
35
|
+
try {
|
|
36
|
+
await deliver(options.notifiers, channel, formatNotification(event, options.brand));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
options.logger?.warn({
|
|
39
|
+
err: error,
|
|
40
|
+
channelId: channel.id
|
|
41
|
+
}, "notification failed");
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deliver a site-wide admin alert to project-less channels
|
|
47
|
+
* (e.g. `sys:user-created`, `sys:capture-failed`, `sys:purge-completed`).
|
|
48
|
+
*/
|
|
49
|
+
async function emitSystemNotification(event, options = {}) {
|
|
50
|
+
const targets = (options.channels ?? []).filter((channel) => channel.projectId === null && subscribed(channel, event.event));
|
|
51
|
+
if (targets.length === 0) return;
|
|
52
|
+
await Promise.allSettled(targets.map(async (channel) => {
|
|
53
|
+
try {
|
|
54
|
+
await deliver(options.notifiers, channel, formatSystemNotification(event, options.brand));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
options.logger?.warn({
|
|
57
|
+
err: error,
|
|
58
|
+
channelId: channel.id
|
|
59
|
+
}, "system notification failed");
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { emitNotifications, emitSystemNotification };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=events.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.mjs","names":[],"sources":["../../../src/adapters/notifier/events.ts"],"sourcesContent":["import type { Logger } from \"../../logger.ts\";\nimport { formatNotification, formatSystemNotification } from \"./format.ts\";\nimport type { NotifierProvider } from \"./provider.ts\";\nimport type {\n NotificationBrand,\n NotificationChannel,\n NotificationEvent,\n SystemNotification,\n} from \"./types.ts\";\n\n/** Options for project-event fan-out (inline best-effort, non-fatal). */\nexport interface EmitNotificationsOptions {\n notifiers?: NotifierProvider[];\n channels?: NotificationChannel[];\n brand?: NotificationBrand;\n logger?: Logger;\n}\n\n/** Check an event against a channel's allowlist (empty = all). */\nfunction subscribed(channel: NotificationChannel, event: string): boolean {\n if (!channel.enabled) {\n return false;\n }\n return channel.events.length === 0 || channel.events.includes(event);\n}\n\n/** Resolve a provider for a channel kind, or undefined when unwired. */\nfunction providerFor(\n notifiers: NotifierProvider[] | undefined,\n kind: string,\n): NotifierProvider | undefined {\n return notifiers?.find((candidate) => candidate.metadata.kind === kind);\n}\n\n/** Send one formatted event through a channel's provider (throws on failure). */\nasync function deliver(\n notifiers: NotifierProvider[] | undefined,\n channel: NotificationChannel,\n formatted: ReturnType<typeof formatNotification>,\n): Promise<void> {\n const provider = providerFor(notifiers, channel.provider);\n if (!provider) {\n throw new Error(`notifier missing: ${channel.provider}`);\n }\n const sender = provider.create({ config: channel.config, secret: channel.secret });\n await sender.send({ channel, formatted });\n}\n\n/**\n * Deliver a project event to every subscribed channel.\n * Inline best-effort: `Promise.allSettled`, failures logged and swallowed.\n */\nexport async function emitNotifications(\n event: NotificationEvent,\n options: EmitNotificationsOptions = {},\n): Promise<void> {\n const targets = (options.channels ?? []).filter((channel) => subscribed(channel, event.event));\n if (targets.length === 0) {\n return;\n }\n await Promise.allSettled(\n targets.map(async (channel) => {\n try {\n await deliver(options.notifiers, channel, formatNotification(event, options.brand));\n } catch (error) {\n options.logger?.warn({ err: error, channelId: channel.id }, \"notification failed\");\n }\n }),\n );\n}\n\n/**\n * Deliver a site-wide admin alert to project-less channels\n * (e.g. `sys:user-created`, `sys:capture-failed`, `sys:purge-completed`).\n */\nexport async function emitSystemNotification(\n event: SystemNotification,\n options: EmitNotificationsOptions = {},\n): Promise<void> {\n const targets = (options.channels ?? []).filter(\n (channel) => channel.projectId === null && subscribed(channel, event.event),\n );\n if (targets.length === 0) {\n return;\n }\n await Promise.allSettled(\n targets.map(async (channel) => {\n try {\n await deliver(options.notifiers, channel, formatSystemNotification(event, options.brand));\n } catch (error) {\n options.logger?.warn({ err: error, channelId: channel.id }, \"system notification failed\");\n }\n }),\n );\n}\n"],"mappings":";;;;;;AAmBA,SAAS,WAAW,SAA8B,OAAwB;CACxE,IAAI,CAAC,QAAQ,SACX,OAAO;CAET,OAAO,QAAQ,OAAO,WAAW,KAAK,QAAQ,OAAO,SAAS,KAAK;AACrE;;AAGA,SAAS,YACP,WACA,MAC8B;CAC9B,OAAO,WAAW,MAAM,cAAc,UAAU,SAAS,SAAS,IAAI;AACxE;;AAGA,eAAe,QACb,WACA,SACA,WACe;CACf,MAAM,WAAW,YAAY,WAAW,QAAQ,QAAQ;CACxD,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,qBAAqB,QAAQ,UAAU;CAGzD,MADe,SAAS,OAAO;EAAE,QAAQ,QAAQ;EAAQ,QAAQ,QAAQ;CAAO,CACrE,CAAC,CAAC,KAAK;EAAE;EAAS;CAAU,CAAC;AAC1C;;;;;AAMA,eAAsB,kBACpB,OACA,UAAoC,CAAC,GACtB;CACf,MAAM,WAAW,QAAQ,YAAY,CAAC,EAAA,CAAG,QAAQ,YAAY,WAAW,SAAS,MAAM,KAAK,CAAC;CAC7F,IAAI,QAAQ,WAAW,GACrB;CAEF,MAAM,QAAQ,WACZ,QAAQ,IAAI,OAAO,YAAY;EAC7B,IAAI;GACF,MAAM,QAAQ,QAAQ,WAAW,SAAS,mBAAmB,OAAO,QAAQ,KAAK,CAAC;EACpF,SAAS,OAAO;GACd,QAAQ,QAAQ,KAAK;IAAE,KAAK;IAAO,WAAW,QAAQ;GAAG,GAAG,qBAAqB;EACnF;CACF,CAAC,CACH;AACF;;;;;AAMA,eAAsB,uBACpB,OACA,UAAoC,CAAC,GACtB;CACf,MAAM,WAAW,QAAQ,YAAY,CAAC,EAAA,CAAG,QACtC,YAAY,QAAQ,cAAc,QAAQ,WAAW,SAAS,MAAM,KAAK,CAC5E;CACA,IAAI,QAAQ,WAAW,GACrB;CAEF,MAAM,QAAQ,WACZ,QAAQ,IAAI,OAAO,YAAY;EAC7B,IAAI;GACF,MAAM,QAAQ,QAAQ,WAAW,SAAS,yBAAyB,OAAO,QAAQ,KAAK,CAAC;EAC1F,SAAS,OAAO;GACd,QAAQ,QAAQ,KAAK;IAAE,KAAK;IAAO,WAAW,QAAQ;GAAG,GAAG,4BAA4B;EAC1F;CACF,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { ChannelToggles, FormattedNotification, NotificationBrand, NotificationEvent, SystemNotification } from "./types.mjs";
|
|
4
|
+
//#region src/adapters/notifier/format.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Format a project event into subject/text/markdown/html using brand +
|
|
7
|
+
* channel toggles. Pure (no I/O) so settings preview and senders share it.
|
|
8
|
+
*/
|
|
9
|
+
declare function formatNotification(event: NotificationEvent, brand?: NotificationBrand, toggles?: ChannelToggles): FormattedNotification;
|
|
10
|
+
/** Format a site-wide admin alert (no project scope, always verbose). */
|
|
11
|
+
declare function formatSystemNotification(event: SystemNotification, brand?: NotificationBrand): FormattedNotification;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { formatNotification, formatSystemNotification };
|
|
14
|
+
//# sourceMappingURL=format.d.mts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/adapters/notifier/format.ts
|
|
5
|
+
/** Escape HTML special chars in free-form values (commit messages, names). */
|
|
6
|
+
function esc(value) {
|
|
7
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
8
|
+
}
|
|
9
|
+
/** Read a string field from event data without throwing on shapes. */
|
|
10
|
+
function str(data, key) {
|
|
11
|
+
const value = data[key];
|
|
12
|
+
return typeof value === "string" ? value : "";
|
|
13
|
+
}
|
|
14
|
+
/** Resolve toggle defaults (Brand + toggles v1: no custom templates). */
|
|
15
|
+
function togglesOf(toggles) {
|
|
16
|
+
return {
|
|
17
|
+
style: toggles?.style ?? "compact",
|
|
18
|
+
subjectPrefix: toggles?.subjectPrefix ?? "[Shelf]",
|
|
19
|
+
includeAuthor: toggles?.includeAuthor ?? true,
|
|
20
|
+
includeMessage: toggles?.includeMessage ?? true,
|
|
21
|
+
includeCounts: toggles?.includeCounts ?? true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** One-line summary for an event (used in subject + compact bodies). */
|
|
25
|
+
function headline(event) {
|
|
26
|
+
const branch = str(event.data, "gitBranch") || str(event.data, "branch");
|
|
27
|
+
const status = str(event.data, "status");
|
|
28
|
+
const story = str(event.data, "storyId");
|
|
29
|
+
if (story) return `${event.event} ${story}${branch ? ` on ${branch}` : ""}`;
|
|
30
|
+
return `${event.event}${branch ? ` on ${branch}` : ""}${status ? ` (${status})` : ""}`;
|
|
31
|
+
}
|
|
32
|
+
/** Append author/message/counts lines for verbose bodies. */
|
|
33
|
+
function appendDetails(lines, event, resolved) {
|
|
34
|
+
if (resolved.style !== "verbose") return;
|
|
35
|
+
const author = str(event.data, "authorName") || str(event.data, "authorEmail");
|
|
36
|
+
if (resolved.includeAuthor && author) lines.push(`by ${author}`);
|
|
37
|
+
const message = str(event.data, "message");
|
|
38
|
+
if (resolved.includeMessage && message) lines.push(message);
|
|
39
|
+
appendCounts(lines, event, resolved);
|
|
40
|
+
}
|
|
41
|
+
/** Append numeric count summary (snapshot/changed/approved/rejected). */
|
|
42
|
+
function appendCounts(lines, event, resolved) {
|
|
43
|
+
if (!resolved.includeCounts) return;
|
|
44
|
+
const counts = [
|
|
45
|
+
"snapshotCount",
|
|
46
|
+
"changedCount",
|
|
47
|
+
"approvedCount",
|
|
48
|
+
"rejectedCount"
|
|
49
|
+
].map((key) => {
|
|
50
|
+
const value = event.data[key];
|
|
51
|
+
return typeof value === "number" ? `${key}=${value}` : "";
|
|
52
|
+
}).filter((part) => part !== "").join(" ");
|
|
53
|
+
if (counts) lines.push(counts);
|
|
54
|
+
}
|
|
55
|
+
/** Render text/html envelopes around body lines plus brand footer. */
|
|
56
|
+
function envelope(subject, lines, brand) {
|
|
57
|
+
const name = brand?.name ?? "StoryShelf";
|
|
58
|
+
const text = lines.join("\n");
|
|
59
|
+
const footer = brand?.footerText ?? `Sent by ${name}`;
|
|
60
|
+
const htmlBody = lines.map((line) => esc(line)).join("<br/>");
|
|
61
|
+
const logo = brand?.logo ? `<img src="${esc(brand.logo)}" alt="" width="24"/>` : "";
|
|
62
|
+
return {
|
|
63
|
+
subject,
|
|
64
|
+
text: `${text}\n\n${footer}`,
|
|
65
|
+
markdown: `${text}\n\n_${esc(footer)}_`,
|
|
66
|
+
html: `${logo}<p>${htmlBody}</p><p><small>${esc(footer)}</small></p>`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format a project event into subject/text/markdown/html using brand +
|
|
71
|
+
* channel toggles. Pure (no I/O) so settings preview and senders share it.
|
|
72
|
+
*/
|
|
73
|
+
function formatNotification(event, brand, toggles) {
|
|
74
|
+
const resolved = togglesOf(toggles);
|
|
75
|
+
const name = brand?.name ?? "StoryShelf";
|
|
76
|
+
const subject = `${resolved.subjectPrefix} ${headline(event)}`.trim();
|
|
77
|
+
const lines = [`${name}: ${headline(event)}`];
|
|
78
|
+
appendDetails(lines, event, resolved);
|
|
79
|
+
if (brand?.reviewUrl) lines.push(brand.reviewUrl);
|
|
80
|
+
return envelope(subject, lines, brand);
|
|
81
|
+
}
|
|
82
|
+
/** Format a site-wide admin alert (no project scope, always verbose). */
|
|
83
|
+
function formatSystemNotification(event, brand) {
|
|
84
|
+
return formatNotification({
|
|
85
|
+
event: event.event,
|
|
86
|
+
data: event.data,
|
|
87
|
+
timestamp: event.timestamp
|
|
88
|
+
}, brand, {
|
|
89
|
+
style: "verbose",
|
|
90
|
+
subjectPrefix: "[Shelf sys]"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { formatNotification, formatSystemNotification };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=format.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.mjs","names":[],"sources":["../../../src/adapters/notifier/format.ts"],"sourcesContent":["import type {\n ChannelToggles,\n FormattedNotification,\n NotificationBrand,\n NotificationEvent,\n SystemNotification,\n} from \"./types.ts\";\n\n/** Escape HTML special chars in free-form values (commit messages, names). */\nfunction esc(value: string): string {\n return value.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\n}\n\n/** Read a string field from event data without throwing on shapes. */\nfunction str(data: Record<string, unknown>, key: string): string {\n const value = data[key];\n return typeof value === \"string\" ? value : \"\";\n}\n\n/** Resolve toggle defaults (Brand + toggles v1: no custom templates). */\nfunction togglesOf(toggles?: ChannelToggles): Required<ChannelToggles> {\n return {\n style: toggles?.style ?? \"compact\",\n subjectPrefix: toggles?.subjectPrefix ?? \"[Shelf]\",\n includeAuthor: toggles?.includeAuthor ?? true,\n includeMessage: toggles?.includeMessage ?? true,\n includeCounts: toggles?.includeCounts ?? true,\n };\n}\n\n/** One-line summary for an event (used in subject + compact bodies). */\nfunction headline(event: NotificationEvent): string {\n const branch = str(event.data, \"gitBranch\") || str(event.data, \"branch\");\n const status = str(event.data, \"status\");\n const story = str(event.data, \"storyId\");\n if (story) {\n return `${event.event} ${story}${branch ? ` on ${branch}` : \"\"}`;\n }\n return `${event.event}${branch ? ` on ${branch}` : \"\"}${status ? ` (${status})` : \"\"}`;\n}\n\n/** Append author/message/counts lines for verbose bodies. */\nfunction appendDetails(\n lines: string[],\n event: NotificationEvent,\n resolved: Required<ChannelToggles>,\n): void {\n if (resolved.style !== \"verbose\") {\n return;\n }\n const author = str(event.data, \"authorName\") || str(event.data, \"authorEmail\");\n if (resolved.includeAuthor && author) {\n lines.push(`by ${author}`);\n }\n const message = str(event.data, \"message\");\n if (resolved.includeMessage && message) {\n lines.push(message);\n }\n appendCounts(lines, event, resolved);\n}\n\n/** Append numeric count summary (snapshot/changed/approved/rejected). */\nfunction appendCounts(\n lines: string[],\n event: NotificationEvent,\n resolved: Required<ChannelToggles>,\n): void {\n if (!resolved.includeCounts) {\n return;\n }\n const counts = [\"snapshotCount\", \"changedCount\", \"approvedCount\", \"rejectedCount\"]\n .map((key) => {\n const value = event.data[key];\n return typeof value === \"number\" ? `${key}=${value}` : \"\";\n })\n .filter((part) => part !== \"\")\n .join(\" \");\n if (counts) {\n lines.push(counts);\n }\n}\n\n/** Render text/html envelopes around body lines plus brand footer. */\nfunction envelope(\n subject: string,\n lines: string[],\n brand?: NotificationBrand,\n): FormattedNotification {\n const name = brand?.name ?? \"StoryShelf\";\n const text = lines.join(\"\\n\");\n const footer = brand?.footerText ?? `Sent by ${name}`;\n const htmlBody = lines.map((line) => esc(line)).join(\"<br/>\");\n const logo = brand?.logo ? `<img src=\"${esc(brand.logo)}\" alt=\"\" width=\"24\"/>` : \"\";\n return {\n subject,\n text: `${text}\\n\\n${footer}`,\n markdown: `${text}\\n\\n_${esc(footer)}_`,\n html: `${logo}<p>${htmlBody}</p><p><small>${esc(footer)}</small></p>`,\n };\n}\n\n/**\n * Format a project event into subject/text/markdown/html using brand +\n * channel toggles. Pure (no I/O) so settings preview and senders share it.\n */\nexport function formatNotification(\n event: NotificationEvent,\n brand?: NotificationBrand,\n toggles?: ChannelToggles,\n): FormattedNotification {\n const resolved = togglesOf(toggles);\n const name = brand?.name ?? \"StoryShelf\";\n const subject = `${resolved.subjectPrefix} ${headline(event)}`.trim();\n const lines = [`${name}: ${headline(event)}`];\n appendDetails(lines, event, resolved);\n if (brand?.reviewUrl) {\n lines.push(brand.reviewUrl);\n }\n return envelope(subject, lines, brand);\n}\n\n/** Format a site-wide admin alert (no project scope, always verbose). */\nexport function formatSystemNotification(\n event: SystemNotification,\n brand?: NotificationBrand,\n): FormattedNotification {\n return formatNotification(\n { event: event.event, data: event.data, timestamp: event.timestamp },\n brand,\n { style: \"verbose\", subjectPrefix: \"[Shelf sys]\" },\n );\n}\n"],"mappings":";;;;;AASA,SAAS,IAAI,OAAuB;CAClC,OAAO,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,WAAW,KAAK,MAAM;AACtF;;AAGA,SAAS,IAAI,MAA+B,KAAqB;CAC/D,MAAM,QAAQ,KAAK;CACnB,OAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;;AAGA,SAAS,UAAU,SAAoD;CACrE,OAAO;EACL,OAAO,SAAS,SAAS;EACzB,eAAe,SAAS,iBAAiB;EACzC,eAAe,SAAS,iBAAiB;EACzC,gBAAgB,SAAS,kBAAkB;EAC3C,eAAe,SAAS,iBAAiB;CAC3C;AACF;;AAGA,SAAS,SAAS,OAAkC;CAClD,MAAM,SAAS,IAAI,MAAM,MAAM,WAAW,KAAK,IAAI,MAAM,MAAM,QAAQ;CACvE,MAAM,SAAS,IAAI,MAAM,MAAM,QAAQ;CACvC,MAAM,QAAQ,IAAI,MAAM,MAAM,SAAS;CACvC,IAAI,OACF,OAAO,GAAG,MAAM,MAAM,GAAG,QAAQ,SAAS,OAAO,WAAW;CAE9D,OAAO,GAAG,MAAM,QAAQ,SAAS,OAAO,WAAW,KAAK,SAAS,KAAK,OAAO,KAAK;AACpF;;AAGA,SAAS,cACP,OACA,OACA,UACM;CACN,IAAI,SAAS,UAAU,WACrB;CAEF,MAAM,SAAS,IAAI,MAAM,MAAM,YAAY,KAAK,IAAI,MAAM,MAAM,aAAa;CAC7E,IAAI,SAAS,iBAAiB,QAC5B,MAAM,KAAK,MAAM,QAAQ;CAE3B,MAAM,UAAU,IAAI,MAAM,MAAM,SAAS;CACzC,IAAI,SAAS,kBAAkB,SAC7B,MAAM,KAAK,OAAO;CAEpB,aAAa,OAAO,OAAO,QAAQ;AACrC;;AAGA,SAAS,aACP,OACA,OACA,UACM;CACN,IAAI,CAAC,SAAS,eACZ;CAEF,MAAM,SAAS;EAAC;EAAiB;EAAgB;EAAiB;CAAe,CAAC,CAC/E,KAAK,QAAQ;EACZ,MAAM,QAAQ,MAAM,KAAK;EACzB,OAAO,OAAO,UAAU,WAAW,GAAG,IAAI,GAAG,UAAU;CACzD,CAAC,CAAC,CACD,QAAQ,SAAS,SAAS,EAAE,CAAC,CAC7B,KAAK,GAAG;CACX,IAAI,QACF,MAAM,KAAK,MAAM;AAErB;;AAGA,SAAS,SACP,SACA,OACA,OACuB;CACvB,MAAM,OAAO,OAAO,QAAQ;CAC5B,MAAM,OAAO,MAAM,KAAK,IAAI;CAC5B,MAAM,SAAS,OAAO,cAAc,WAAW;CAC/C,MAAM,WAAW,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO;CAC5D,MAAM,OAAO,OAAO,OAAO,aAAa,IAAI,MAAM,IAAI,EAAE,yBAAyB;CACjF,OAAO;EACL;EACA,MAAM,GAAG,KAAK,MAAM;EACpB,UAAU,GAAG,KAAK,OAAO,IAAI,MAAM,EAAE;EACrC,MAAM,GAAG,KAAK,KAAK,SAAS,gBAAgB,IAAI,MAAM,EAAE;CAC1D;AACF;;;;;AAMA,SAAgB,mBACd,OACA,OACA,SACuB;CACvB,MAAM,WAAW,UAAU,OAAO;CAClC,MAAM,OAAO,OAAO,QAAQ;CAC5B,MAAM,UAAU,GAAG,SAAS,cAAc,GAAG,SAAS,KAAK,IAAI,KAAK;CACpE,MAAM,QAAQ,CAAC,GAAG,KAAK,IAAI,SAAS,KAAK,GAAG;CAC5C,cAAc,OAAO,OAAO,QAAQ;CACpC,IAAI,OAAO,WACT,MAAM,KAAK,MAAM,SAAS;CAE5B,OAAO,SAAS,SAAS,OAAO,KAAK;AACvC;;AAGA,SAAgB,yBACd,OACA,OACuB;CACvB,OAAO,mBACL;EAAE,OAAO,MAAM;EAAO,MAAM,MAAM;EAAM,WAAW,MAAM;CAAU,GACnE,OACA;EAAE,OAAO;EAAW,eAAe;CAAc,CACnD;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { FormattedNotification, NotificationChannel } from "./types.mjs";
|
|
4
|
+
import { Adapter, AdapterMetadata } from "../../adapter/metadata.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Logger } from "pino";
|
|
7
|
+
//#region src/adapters/notifier/provider.d.ts
|
|
8
|
+
/** Notifier-specific metadata (adds logo + config validation schema). */
|
|
9
|
+
interface NotifierMetadata extends AdapterMetadata {
|
|
10
|
+
readonly category: "notifier";
|
|
11
|
+
readonly logo?: string;
|
|
12
|
+
readonly schema: z.ZodType;
|
|
13
|
+
}
|
|
14
|
+
/** Descriptor — registered at startup, validates per-channel config. */
|
|
15
|
+
interface NotifierProvider extends Adapter<NotifierMetadata> {
|
|
16
|
+
create(opts: {
|
|
17
|
+
config: unknown;
|
|
18
|
+
secret?: string;
|
|
19
|
+
logger?: Logger;
|
|
20
|
+
}): NotifierAdapter;
|
|
21
|
+
}
|
|
22
|
+
/** Runtime — bound to one channel config + decrypted secret, sends one message. */
|
|
23
|
+
interface NotifierAdapter extends Adapter<NotifierMetadata> {
|
|
24
|
+
send(input: {
|
|
25
|
+
channel: NotificationChannel;
|
|
26
|
+
formatted: FormattedNotification;
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { NotifierAdapter, NotifierMetadata, NotifierProvider };
|
|
32
|
+
//# sourceMappingURL=provider.d.mts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/adapters/notifier/types.d.ts
|
|
4
|
+
/** Shared event envelope for human-facing notifications. */
|
|
5
|
+
interface NotificationEvent {
|
|
6
|
+
event: string;
|
|
7
|
+
projectId?: string;
|
|
8
|
+
projectSlug?: string;
|
|
9
|
+
data: Record<string, unknown>;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
}
|
|
12
|
+
/** Per-channel display toggles (Brand + toggles v1, no custom templates). */
|
|
13
|
+
interface ChannelToggles {
|
|
14
|
+
style?: "compact" | "verbose";
|
|
15
|
+
subjectPrefix?: string;
|
|
16
|
+
includeAuthor?: boolean;
|
|
17
|
+
includeMessage?: boolean;
|
|
18
|
+
includeCounts?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Brand inputs reused from UIConfig plus the review URL. */
|
|
21
|
+
interface NotificationBrand {
|
|
22
|
+
name?: string;
|
|
23
|
+
logo?: string;
|
|
24
|
+
reviewUrl?: string;
|
|
25
|
+
fromEmail?: string;
|
|
26
|
+
fromName?: string;
|
|
27
|
+
footerText?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Formatted output a provider transports (email body, chat markdown). */
|
|
30
|
+
interface FormattedNotification {
|
|
31
|
+
subject: string;
|
|
32
|
+
text: string;
|
|
33
|
+
markdown: string;
|
|
34
|
+
html: string;
|
|
35
|
+
}
|
|
36
|
+
/** A configured project-level channel row (storage-agnostic view). */
|
|
37
|
+
interface NotificationChannel {
|
|
38
|
+
id: string;
|
|
39
|
+
projectId: string | null;
|
|
40
|
+
provider: string;
|
|
41
|
+
config: Record<string, unknown>;
|
|
42
|
+
secret?: string;
|
|
43
|
+
events: string[];
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** App-level (site-wide) admin alert — no project scope. */
|
|
47
|
+
interface SystemNotification {
|
|
48
|
+
event: SystemEventName;
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
timestamp: string;
|
|
51
|
+
}
|
|
52
|
+
/** Site-wide events for admins (new users, failures, purge). */
|
|
53
|
+
type SystemEventName = "sys:user-created" | "sys:invite-issued" | "sys:auth-failed" | "sys:capture-failed" | "sys:purge-completed";
|
|
54
|
+
//#endregion
|
|
55
|
+
export { ChannelToggles, FormattedNotification, NotificationBrand, NotificationChannel, NotificationEvent, SystemEventName, SystemNotification };
|
|
56
|
+
//# sourceMappingURL=types.d.mts.map
|