@storyshelf/core 0.3.2 → 0.5.4
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 -70
- 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
package/dist/tracing.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
5
|
+
//#region src/tracing.ts
|
|
6
|
+
/**
|
|
7
|
+
* Internal span and propagation helpers over `@opentelemetry/api`.
|
|
8
|
+
*
|
|
9
|
+
* Not exported from the package surface (see `package.json` exports): other
|
|
10
|
+
* packages use `@storyshelf/observability`. Without a configured provider
|
|
11
|
+
* every helper is a cheap noop, so call sites need no enabled-checks and
|
|
12
|
+
* the hermetic vitest suite stays network-free.
|
|
13
|
+
*/
|
|
14
|
+
/** Instrumentation scope for core spans. */
|
|
15
|
+
const SCOPE = "storyshelf.core";
|
|
16
|
+
/** W3C traceparent header name. */
|
|
17
|
+
const TRACEPARENT_HEADER = "traceparent";
|
|
18
|
+
/**
|
|
19
|
+
* Run `fn` inside an internal span.
|
|
20
|
+
*
|
|
21
|
+
* On throw the span records the exception, is marked ERROR, and the error
|
|
22
|
+
* is rethrown — mirroring the pino `{ err }` convention.
|
|
23
|
+
*
|
|
24
|
+
* @param name - Span name (`domain.operation`, never a high-cardinality value).
|
|
25
|
+
* @param fn - Work to run with the span active.
|
|
26
|
+
* @param attributes - Fixed span attributes.
|
|
27
|
+
* @param parent - Parent context (defaults to the active context).
|
|
28
|
+
* @returns Whatever `fn` returns.
|
|
29
|
+
*/
|
|
30
|
+
async function withSpan(name, fn, attributes, parent) {
|
|
31
|
+
return await trace.getTracer(SCOPE).startActiveSpan(name, {
|
|
32
|
+
kind: SpanKind.INTERNAL,
|
|
33
|
+
attributes
|
|
34
|
+
}, parent ?? context.active(), async (span) => {
|
|
35
|
+
try {
|
|
36
|
+
return await fn(span);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
span.recordException(error instanceof Error ? error : String(error));
|
|
39
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
40
|
+
throw error;
|
|
41
|
+
} finally {
|
|
42
|
+
span.end();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the parent context from an inbound `traceparent` value.
|
|
48
|
+
*
|
|
49
|
+
* @param traceparent - W3C value from a queue payload; absent keeps context.
|
|
50
|
+
* @returns The extracted remote context, or the active context when absent.
|
|
51
|
+
*/
|
|
52
|
+
function parentContext(traceparent) {
|
|
53
|
+
if (!traceparent) return context.active();
|
|
54
|
+
return propagation.extract(context.active(), { [TRACEPARENT_HEADER]: traceparent });
|
|
55
|
+
}
|
|
56
|
+
/** Inject the active span context into a mutable header record. */
|
|
57
|
+
function injectTraceContext(headers) {
|
|
58
|
+
propagation.inject(context.active(), headers);
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { injectTraceContext, parentContext, withSpan };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=tracing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracing.mjs","names":[],"sources":["../src/tracing.ts"],"sourcesContent":["/**\n * Internal span and propagation helpers over `@opentelemetry/api`.\n *\n * Not exported from the package surface (see `package.json` exports): other\n * packages use `@storyshelf/observability`. Without a configured provider\n * every helper is a cheap noop, so call sites need no enabled-checks and\n * the hermetic vitest suite stays network-free.\n */\nimport {\n SpanKind,\n SpanStatusCode,\n context,\n propagation,\n trace,\n type Attributes,\n type Context,\n type Span,\n} from \"@opentelemetry/api\";\n\n/** Instrumentation scope for core spans. */\nconst SCOPE = \"storyshelf.core\";\n\n/** W3C traceparent header name. */\nconst TRACEPARENT_HEADER = \"traceparent\";\n\n/**\n * Run `fn` inside an internal span.\n *\n * On throw the span records the exception, is marked ERROR, and the error\n * is rethrown — mirroring the pino `{ err }` convention.\n *\n * @param name - Span name (`domain.operation`, never a high-cardinality value).\n * @param fn - Work to run with the span active.\n * @param attributes - Fixed span attributes.\n * @param parent - Parent context (defaults to the active context).\n * @returns Whatever `fn` returns.\n */\nexport async function withSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T>,\n attributes?: Attributes,\n parent?: Context,\n): Promise<T> {\n const tracer = trace.getTracer(SCOPE);\n return await tracer.startActiveSpan(\n name,\n { kind: SpanKind.INTERNAL, attributes },\n parent ?? context.active(),\n async (span) => {\n try {\n return await fn(span);\n } catch (error) {\n span.recordException(error instanceof Error ? error : String(error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n );\n}\n\n/**\n * Resolve the parent context from an inbound `traceparent` value.\n *\n * @param traceparent - W3C value from a queue payload; absent keeps context.\n * @returns The extracted remote context, or the active context when absent.\n */\nexport function parentContext(traceparent?: string): Context {\n if (!traceparent) {\n return context.active();\n }\n return propagation.extract(context.active(), { [TRACEPARENT_HEADER]: traceparent });\n}\n\n/** Inject the active span context into a mutable header record. */\nexport function injectTraceContext(headers: Record<string, string>): void {\n propagation.inject(context.active(), headers);\n}\n\n/**\n * Read the current W3C `traceparent` value for queue payloads.\n *\n * @returns The header value, or undefined when no span is active.\n */\nexport function currentTraceparent(): string | undefined {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier);\n return carrier[TRACEPARENT_HEADER];\n}\n"],"mappings":";;;;;;;;;;;;;;AAoBA,MAAM,QAAQ;;AAGd,MAAM,qBAAqB;;;;;;;;;;;;;AAc3B,eAAsB,SACpB,MACA,IACA,YACA,QACY;CAEZ,OAAO,MADQ,MAAM,UAAU,KACb,CAAC,CAAC,gBAClB,MACA;EAAE,MAAM,SAAS;EAAU;CAAW,GACtC,UAAU,QAAQ,OAAO,GACzB,OAAO,SAAS;EACd,IAAI;GACF,OAAO,MAAM,GAAG,IAAI;EACtB,SAAS,OAAO;GACd,KAAK,gBAAgB,iBAAiB,QAAQ,QAAQ,OAAO,KAAK,CAAC;GACnE,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC7C,MAAM;EACR,UAAU;GACR,KAAK,IAAI;EACX;CACF,CACF;AACF;;;;;;;AAQA,SAAgB,cAAc,aAA+B;CAC3D,IAAI,CAAC,aACH,OAAO,QAAQ,OAAO;CAExB,OAAO,YAAY,QAAQ,QAAQ,OAAO,GAAG,GAAG,qBAAqB,YAAY,CAAC;AACpF;;AAGA,SAAgB,mBAAmB,SAAuC;CACxE,YAAY,OAAO,QAAQ,OAAO,GAAG,OAAO;AAC9C"}
|
package/dist/types.d.mts
CHANGED
|
@@ -9,22 +9,55 @@ type BuildStatus = (typeof BUILD_STATUSES)[number];
|
|
|
9
9
|
declare const SNAPSHOT_STATUSES: readonly ["pending", "new", "unchanged", "changed", "approved", "rejected"];
|
|
10
10
|
/** Review status of a snapshot. */
|
|
11
11
|
type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Build statuses considered terminal for retention purging.
|
|
14
|
+
* Only builds that have finished review (`approved`, `rejected`, `failed`)
|
|
15
|
+
* are eligible for TTL-based cleanup; `pending`/`reviewing` builds are kept.
|
|
16
|
+
*/
|
|
13
17
|
declare const TERMINAL_BUILD_STATUSES: readonly BuildStatus[];
|
|
14
18
|
/** All site-wide user roles. */
|
|
15
|
-
declare const SITE_ROLES: readonly ["admin", "member"];
|
|
19
|
+
declare const SITE_ROLES: readonly ["admin", "member", "viewer"];
|
|
16
20
|
/** Site-wide role of a user. */
|
|
17
21
|
type SiteRole = (typeof SITE_ROLES)[number];
|
|
22
|
+
/** Shared session cookie name used by auth (ADR 0008). */
|
|
23
|
+
declare const SESSION_COOKIE = "storyshelf_session";
|
|
24
|
+
/** An authenticated user of the platform. */
|
|
25
|
+
interface AuthUser {
|
|
26
|
+
/** User ID. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** User email. */
|
|
29
|
+
email: string;
|
|
30
|
+
/** Display name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Optional avatar URL. */
|
|
33
|
+
avatarUrl?: string;
|
|
34
|
+
/** Site-wide role. */
|
|
35
|
+
role: SiteRole;
|
|
36
|
+
/**
|
|
37
|
+
* Identity-provider group memberships (names or IDs, provider-dependent).
|
|
38
|
+
* Populated when the provider exposes group claims; absent otherwise.
|
|
39
|
+
* Drives group-to-role mapping at login.
|
|
40
|
+
*/
|
|
41
|
+
groups?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Login method that minted this identity.
|
|
44
|
+
* Matches an engine login method id; absent for single-method deployments.
|
|
45
|
+
*/
|
|
46
|
+
providerId?: string;
|
|
47
|
+
}
|
|
18
48
|
/** All per-project membership roles. */
|
|
19
49
|
declare const PROJECT_ROLES: readonly ["admin", "approver", "developer", "viewer"];
|
|
20
50
|
/** Per-project role of a member. */
|
|
21
51
|
type ProjectRole = (typeof PROJECT_ROLES)[number];
|
|
22
52
|
/** Label type keys seeded for every new project. */
|
|
23
53
|
declare const SEEDED_LABEL_KEYS: readonly ["branch", "persistent", "pr", "mr", "jira", "linear", "figma", "custom"];
|
|
24
|
-
/** Label type keys reserved for internal
|
|
54
|
+
/** Label type keys reserved for internal bookkeeping (cannot be created by users). */
|
|
25
55
|
declare const RESERVED_LABEL_KEYS: readonly ["build"];
|
|
26
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Label key that marks a build as exempt from retention purging.
|
|
58
|
+
* Attach `persistent=true` to keep a build indefinitely, even after its TTL expires.
|
|
59
|
+
*/
|
|
27
60
|
declare const PERSISTENT_LABEL_KEY = "persistent";
|
|
28
61
|
//#endregion
|
|
29
|
-
export { BUILD_STATUSES, BuildStatus, PERSISTENT_LABEL_KEY, PROJECT_ROLES, ProjectRole, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, SiteRole, SnapshotStatus, TERMINAL_BUILD_STATUSES };
|
|
62
|
+
export { AuthUser, BUILD_STATUSES, BuildStatus, PERSISTENT_LABEL_KEY, PROJECT_ROLES, ProjectRole, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SESSION_COOKIE, SITE_ROLES, SNAPSHOT_STATUSES, SiteRole, SnapshotStatus, TERMINAL_BUILD_STATUSES };
|
|
30
63
|
//# sourceMappingURL=types.d.mts.map
|
package/dist/types.mjs
CHANGED
|
@@ -21,14 +21,24 @@ const SNAPSHOT_STATUSES = [
|
|
|
21
21
|
"approved",
|
|
22
22
|
"rejected"
|
|
23
23
|
];
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Build statuses considered terminal for retention purging.
|
|
26
|
+
* Only builds that have finished review (`approved`, `rejected`, `failed`)
|
|
27
|
+
* are eligible for TTL-based cleanup; `pending`/`reviewing` builds are kept.
|
|
28
|
+
*/
|
|
25
29
|
const TERMINAL_BUILD_STATUSES = [
|
|
26
30
|
"approved",
|
|
27
31
|
"rejected",
|
|
28
32
|
"failed"
|
|
29
33
|
];
|
|
30
34
|
/** All site-wide user roles. */
|
|
31
|
-
const SITE_ROLES = [
|
|
35
|
+
const SITE_ROLES = [
|
|
36
|
+
"admin",
|
|
37
|
+
"member",
|
|
38
|
+
"viewer"
|
|
39
|
+
];
|
|
40
|
+
/** Shared session cookie name used by auth (ADR 0008). */
|
|
41
|
+
const SESSION_COOKIE = "storyshelf_session";
|
|
32
42
|
/** All per-project membership roles. */
|
|
33
43
|
const PROJECT_ROLES = [
|
|
34
44
|
"admin",
|
|
@@ -47,11 +57,14 @@ const SEEDED_LABEL_KEYS = [
|
|
|
47
57
|
"figma",
|
|
48
58
|
"custom"
|
|
49
59
|
];
|
|
50
|
-
/** Label type keys reserved for internal
|
|
60
|
+
/** Label type keys reserved for internal bookkeeping (cannot be created by users). */
|
|
51
61
|
const RESERVED_LABEL_KEYS = ["build"];
|
|
52
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Label key that marks a build as exempt from retention purging.
|
|
64
|
+
* Attach `persistent=true` to keep a build indefinitely, even after its TTL expires.
|
|
65
|
+
*/
|
|
53
66
|
const PERSISTENT_LABEL_KEY = "persistent";
|
|
54
67
|
//#endregion
|
|
55
|
-
export { BUILD_STATUSES, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, TERMINAL_BUILD_STATUSES };
|
|
68
|
+
export { BUILD_STATUSES, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SESSION_COOKIE, SITE_ROLES, SNAPSHOT_STATUSES, TERMINAL_BUILD_STATUSES };
|
|
56
69
|
|
|
57
70
|
//# sourceMappingURL=types.mjs.map
|
package/dist/types.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/** All lifecycle statuses a build can carry. */\nexport const BUILD_STATUSES = [\n \"pending\",\n \"capturing\",\n \"comparing\",\n \"reviewing\",\n \"approved\",\n \"rejected\",\n \"failed\",\n] as const;\n\n/** Lifecycle status of a build. */\nexport type BuildStatus = (typeof BUILD_STATUSES)[number];\n\n/** All review statuses a snapshot can carry. */\nexport const SNAPSHOT_STATUSES = [\n \"pending\",\n \"new\",\n \"unchanged\",\n \"changed\",\n \"approved\",\n \"rejected\",\n] as const;\n\n/** Review status of a snapshot. */\nexport type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];\n\n
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/** All lifecycle statuses a build can carry. */\nexport const BUILD_STATUSES = [\n \"pending\",\n \"capturing\",\n \"comparing\",\n \"reviewing\",\n \"approved\",\n \"rejected\",\n \"failed\",\n] as const;\n\n/** Lifecycle status of a build. */\nexport type BuildStatus = (typeof BUILD_STATUSES)[number];\n\n/** All review statuses a snapshot can carry. */\nexport const SNAPSHOT_STATUSES = [\n \"pending\",\n \"new\",\n \"unchanged\",\n \"changed\",\n \"approved\",\n \"rejected\",\n] as const;\n\n/** Review status of a snapshot. */\nexport type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];\n\n/**\n * Build statuses considered terminal for retention purging.\n * Only builds that have finished review (`approved`, `rejected`, `failed`)\n * are eligible for TTL-based cleanup; `pending`/`reviewing` builds are kept.\n */\nexport const TERMINAL_BUILD_STATUSES: readonly BuildStatus[] = [\"approved\", \"rejected\", \"failed\"];\n\n/** All site-wide user roles. */\nexport const SITE_ROLES = [\"admin\", \"member\", \"viewer\"] as const;\n\n/** Site-wide role of a user. */\nexport type SiteRole = (typeof SITE_ROLES)[number];\n\n/** Shared session cookie name used by auth (ADR 0008). */\nexport const SESSION_COOKIE = \"storyshelf_session\";\n\n/** An authenticated user of the platform. */\nexport interface AuthUser {\n /** User ID. */\n id: string;\n /** User email. */\n email: string;\n /** Display name. */\n name: string;\n /** Optional avatar URL. */\n avatarUrl?: string;\n /** Site-wide role. */\n role: SiteRole;\n /**\n * Identity-provider group memberships (names or IDs, provider-dependent).\n * Populated when the provider exposes group claims; absent otherwise.\n * Drives group-to-role mapping at login.\n */\n groups?: string[];\n /**\n * Login method that minted this identity.\n * Matches an engine login method id; absent for single-method deployments.\n */\n providerId?: string;\n}\n\n/** All per-project membership roles. */\nexport const PROJECT_ROLES = [\"admin\", \"approver\", \"developer\", \"viewer\"] as const;\n\n/** Per-project role of a member. */\nexport type ProjectRole = (typeof PROJECT_ROLES)[number];\n\n/** Label type keys seeded for every new project. */\nexport const SEEDED_LABEL_KEYS = [\n \"branch\",\n \"persistent\",\n \"pr\",\n \"mr\",\n \"jira\",\n \"linear\",\n \"figma\",\n \"custom\",\n] as const;\n\n/** Label type keys reserved for internal bookkeeping (cannot be created by users). */\nexport const RESERVED_LABEL_KEYS = [\"build\"] as const;\n\n/**\n * Label key that marks a build as exempt from retention purging.\n * Attach `persistent=true` to keep a build indefinitely, even after its TTL expires.\n */\nexport const PERSISTENT_LABEL_KEY = \"persistent\";\n"],"mappings":";;;;;AACA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAMA,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;AAUA,MAAa,0BAAkD;CAAC;CAAY;CAAY;AAAQ;;AAGhG,MAAa,aAAa;CAAC;CAAS;CAAU;AAAQ;;AAMtD,MAAa,iBAAiB;;AA4B9B,MAAa,gBAAgB;CAAC;CAAS;CAAY;CAAa;AAAQ;;AAMxE,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAa,sBAAsB,CAAC,OAAO;;;;;AAM3C,MAAa,uBAAuB"}
|
package/dist/urls.d.mts
CHANGED
|
@@ -4,15 +4,21 @@ import __tsdown_shims_url from 'node:url';
|
|
|
4
4
|
/** Type-safe URL builder for project pages and published Storybooks. */
|
|
5
5
|
interface UrlBuilder {
|
|
6
6
|
projectsList(): string;
|
|
7
|
+
projects(): string;
|
|
7
8
|
project(slug: string): string;
|
|
9
|
+
projectsNew(): string;
|
|
8
10
|
buildsList(slug: string): string;
|
|
9
11
|
build(slug: string, buildId: string): string;
|
|
12
|
+
buildDiff(slug: string, buildId: string): string;
|
|
13
|
+
library(slug: string): string;
|
|
14
|
+
jobs(slug: string): string;
|
|
10
15
|
labels(slug: string): string;
|
|
11
16
|
label(slug: string, key: string, value: string): string;
|
|
12
17
|
storybook(slug: string): string;
|
|
13
18
|
storybookLabel(slug: string, key: string, value: string): string;
|
|
14
19
|
storybookBuild(slug: string, buildId: string): string;
|
|
15
20
|
settings(slug: string): string;
|
|
21
|
+
short(id: string): string;
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Create a URL builder rooted at the given base URL.
|
|
@@ -22,6 +28,11 @@ interface UrlBuilder {
|
|
|
22
28
|
* @returns The URL builder.
|
|
23
29
|
*/
|
|
24
30
|
declare function createUrlBuilder(baseUrl: string, publishedBaseDomain?: string): UrlBuilder;
|
|
31
|
+
/**
|
|
32
|
+
* Embeddable image URL check for IdP-supplied avatars: `https:` and
|
|
33
|
+
* app-relative paths only, so `javascript:`/`data:` payloads fall back.
|
|
34
|
+
*/
|
|
35
|
+
declare function safeImageUrl(value: string | undefined): string | null;
|
|
25
36
|
//#endregion
|
|
26
|
-
export { UrlBuilder, createUrlBuilder };
|
|
37
|
+
export { UrlBuilder, createUrlBuilder, safeImageUrl };
|
|
27
38
|
//# sourceMappingURL=urls.d.mts.map
|
package/dist/urls.mjs
CHANGED
|
@@ -12,19 +12,38 @@ import.meta.url;
|
|
|
12
12
|
function createUrlBuilder(baseUrl, publishedBaseDomain) {
|
|
13
13
|
const root = baseUrl.replace(/\/+$/u, "");
|
|
14
14
|
return {
|
|
15
|
-
projectsList: () => root,
|
|
15
|
+
projectsList: () => root || "/",
|
|
16
|
+
projects: () => `${root}/projects`,
|
|
16
17
|
project: (slug) => `${root}/projects/${slug}`,
|
|
18
|
+
projectsNew: () => `${root}/projects/new`,
|
|
17
19
|
buildsList: (slug) => `${root}/projects/${slug}/builds`,
|
|
18
20
|
build: (slug, buildId) => `${root}/projects/${slug}/builds/${buildId}`,
|
|
21
|
+
buildDiff: (slug, buildId) => `${root}/projects/${slug}/builds/${buildId}/diff`,
|
|
22
|
+
library: (slug) => `${root}/projects/${slug}/library`,
|
|
23
|
+
jobs: (slug) => `${root}/projects/${slug}/jobs`,
|
|
19
24
|
labels: (slug) => `${root}/projects/${slug}/labels`,
|
|
20
25
|
label: (slug, key, value) => `${root}/projects/${slug}/labels/${encodeURIComponent(key)}/${encodeURI(value)}`,
|
|
21
26
|
storybook: (slug) => publishedBaseDomain ? `https://${slug}.${publishedBaseDomain}` : `${root}/projects/${slug}/storybook`,
|
|
22
27
|
storybookLabel: (slug, key, value) => `${root}/projects/${slug}/storybook/${encodeURIComponent(key)}/${encodeURI(value)}`,
|
|
23
28
|
storybookBuild: (slug, buildId) => publishedBaseDomain ? `https://${buildId}.${slug}.${publishedBaseDomain}` : `${root}/projects/${slug}/storybook/build/${buildId}`,
|
|
24
|
-
settings: (slug) => `${root}/projects/${slug}/settings
|
|
29
|
+
settings: (slug) => `${root}/projects/${slug}/settings`,
|
|
30
|
+
short: (id) => `${root}/_/${id}/`
|
|
25
31
|
};
|
|
26
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Embeddable image URL check for IdP-supplied avatars: `https:` and
|
|
35
|
+
* app-relative paths only, so `javascript:`/`data:` payloads fall back.
|
|
36
|
+
*/
|
|
37
|
+
function safeImageUrl(value) {
|
|
38
|
+
if (!value) return null;
|
|
39
|
+
if (value.startsWith("/") && !value.startsWith("//")) return value;
|
|
40
|
+
try {
|
|
41
|
+
return new URL(value).protocol === "https:" ? value : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
27
46
|
//#endregion
|
|
28
|
-
export { createUrlBuilder };
|
|
47
|
+
export { createUrlBuilder, safeImageUrl };
|
|
29
48
|
|
|
30
49
|
//# sourceMappingURL=urls.mjs.map
|
package/dist/urls.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urls.mjs","names":[],"sources":["../src/urls.ts"],"sourcesContent":["/** Type-safe URL builder for project pages and published Storybooks. */\nexport interface UrlBuilder {\n projectsList(): string;\n project(slug: string): string;\n buildsList(slug: string): string;\n build(slug: string, buildId: string): string;\n labels(slug: string): string;\n label(slug: string, key: string, value: string): string;\n storybook(slug: string): string;\n storybookLabel(slug: string, key: string, value: string): string;\n storybookBuild(slug: string, buildId: string): string;\n settings(slug: string): string;\n}\n\n/**\n * Create a URL builder rooted at the given base URL.\n *\n * @param baseUrl - Public root URL of the shelf server.\n * @param publishedBaseDomain - Optional wildcard domain for published Storybooks.\n * @returns The URL builder.\n */\nexport function createUrlBuilder(baseUrl: string, publishedBaseDomain?: string): UrlBuilder {\n const root = baseUrl.replace(/\\/+$/u, \"\");\n\n return {\n projectsList: () => root,\n project: (slug) => `${root}/projects/${slug}`,\n buildsList: (slug) => `${root}/projects/${slug}/builds`,\n build: (slug, buildId) => `${root}/projects/${slug}/builds/${buildId}`,\n labels: (slug) => `${root}/projects/${slug}/labels`,\n label: (slug, key, value) =>\n `${root}/projects/${slug}/labels/${encodeURIComponent(key)}/${encodeURI(value)}`,\n storybook: (slug) =>\n publishedBaseDomain\n ? `https://${slug}.${publishedBaseDomain}`\n : `${root}/projects/${slug}/storybook`,\n storybookLabel: (slug, key, value) =>\n `${root}/projects/${slug}/storybook/${encodeURIComponent(key)}/${encodeURI(value)}`,\n storybookBuild: (slug, buildId) =>\n publishedBaseDomain\n ? `https://${buildId}.${slug}.${publishedBaseDomain}`\n : `${root}/projects/${slug}/storybook/build/${buildId}`,\n settings: (slug) => `${root}/projects/${slug}/settings`,\n };\n}\n"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"urls.mjs","names":[],"sources":["../src/urls.ts"],"sourcesContent":["/** Type-safe URL builder for project pages and published Storybooks. */\nexport interface UrlBuilder {\n projectsList(): string;\n projects(): string;\n project(slug: string): string;\n projectsNew(): string;\n buildsList(slug: string): string;\n build(slug: string, buildId: string): string;\n buildDiff(slug: string, buildId: string): string;\n library(slug: string): string;\n jobs(slug: string): string;\n labels(slug: string): string;\n label(slug: string, key: string, value: string): string;\n storybook(slug: string): string;\n storybookLabel(slug: string, key: string, value: string): string;\n storybookBuild(slug: string, buildId: string): string;\n settings(slug: string): string;\n short(id: string): string;\n}\n\n/**\n * Create a URL builder rooted at the given base URL.\n *\n * @param baseUrl - Public root URL of the shelf server.\n * @param publishedBaseDomain - Optional wildcard domain for published Storybooks.\n * @returns The URL builder.\n */\nexport function createUrlBuilder(baseUrl: string, publishedBaseDomain?: string): UrlBuilder {\n const root = baseUrl.replace(/\\/+$/u, \"\");\n\n return {\n projectsList: () => root || \"/\",\n projects: () => `${root}/projects`,\n project: (slug) => `${root}/projects/${slug}`,\n projectsNew: () => `${root}/projects/new`,\n buildsList: (slug) => `${root}/projects/${slug}/builds`,\n build: (slug, buildId) => `${root}/projects/${slug}/builds/${buildId}`,\n buildDiff: (slug, buildId) => `${root}/projects/${slug}/builds/${buildId}/diff`,\n library: (slug) => `${root}/projects/${slug}/library`,\n jobs: (slug) => `${root}/projects/${slug}/jobs`,\n labels: (slug) => `${root}/projects/${slug}/labels`,\n label: (slug, key, value) =>\n `${root}/projects/${slug}/labels/${encodeURIComponent(key)}/${encodeURI(value)}`,\n storybook: (slug) =>\n publishedBaseDomain\n ? `https://${slug}.${publishedBaseDomain}`\n : `${root}/projects/${slug}/storybook`,\n storybookLabel: (slug, key, value) =>\n `${root}/projects/${slug}/storybook/${encodeURIComponent(key)}/${encodeURI(value)}`,\n storybookBuild: (slug, buildId) =>\n publishedBaseDomain\n ? `https://${buildId}.${slug}.${publishedBaseDomain}`\n : `${root}/projects/${slug}/storybook/build/${buildId}`,\n settings: (slug) => `${root}/projects/${slug}/settings`,\n short: (id) => `${root}/_/${id}/`,\n };\n}\n\n/**\n * Embeddable image URL check for IdP-supplied avatars: `https:` and\n * app-relative paths only, so `javascript:`/`data:` payloads fall back.\n */\nexport function safeImageUrl(value: string | undefined): string | null {\n if (!value) {\n return null;\n }\n if (value.startsWith(\"/\") && !value.startsWith(\"//\")) {\n return value;\n }\n try {\n const url = new URL(value);\n return url.protocol === \"https:\" ? value : null;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;;;;;;AA2BA,SAAgB,iBAAiB,SAAiB,qBAA0C;CAC1F,MAAM,OAAO,QAAQ,QAAQ,SAAS,EAAE;CAExC,OAAO;EACL,oBAAoB,QAAQ;EAC5B,gBAAgB,GAAG,KAAK;EACxB,UAAU,SAAS,GAAG,KAAK,YAAY;EACvC,mBAAmB,GAAG,KAAK;EAC3B,aAAa,SAAS,GAAG,KAAK,YAAY,KAAK;EAC/C,QAAQ,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,UAAU;EAC7D,YAAY,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,UAAU,QAAQ;EACzE,UAAU,SAAS,GAAG,KAAK,YAAY,KAAK;EAC5C,OAAO,SAAS,GAAG,KAAK,YAAY,KAAK;EACzC,SAAS,SAAS,GAAG,KAAK,YAAY,KAAK;EAC3C,QAAQ,MAAM,KAAK,UACjB,GAAG,KAAK,YAAY,KAAK,UAAU,mBAAmB,GAAG,EAAE,GAAG,UAAU,KAAK;EAC/E,YAAY,SACV,sBACI,WAAW,KAAK,GAAG,wBACnB,GAAG,KAAK,YAAY,KAAK;EAC/B,iBAAiB,MAAM,KAAK,UAC1B,GAAG,KAAK,YAAY,KAAK,aAAa,mBAAmB,GAAG,EAAE,GAAG,UAAU,KAAK;EAClF,iBAAiB,MAAM,YACrB,sBACI,WAAW,QAAQ,GAAG,KAAK,GAAG,wBAC9B,GAAG,KAAK,YAAY,KAAK,mBAAmB;EAClD,WAAW,SAAS,GAAG,KAAK,YAAY,KAAK;EAC7C,QAAQ,OAAO,GAAG,KAAK,KAAK,GAAG;CACjC;AACF;;;;;AAMA,SAAgB,aAAa,OAA0C;CACrE,IAAI,CAAC,OACH,OAAO;CAET,IAAI,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,WAAW,IAAI,GACjD,OAAO;CAET,IAAI;EAEF,OAAO,IADS,IAAI,KACX,CAAC,CAAC,aAAa,WAAW,QAAQ;CAC7C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/utils/encrypt.d.ts
|
|
4
|
+
/** Current server secret plus the one it replaced, kept only to decrypt older rows. */
|
|
5
|
+
interface SecretKeys {
|
|
6
|
+
current: string;
|
|
7
|
+
previous?: string;
|
|
8
|
+
}
|
|
9
|
+
/** What credential models accept: a bare secret, a rotation pair, or nothing. */
|
|
10
|
+
type SecretInput = string | SecretKeys | undefined;
|
|
11
|
+
/** Which key decrypted a payload. */
|
|
12
|
+
type KeyUsed = "current" | "previous";
|
|
13
|
+
/** Thrown when a stored credential cannot be decrypted with any configured key. */
|
|
14
|
+
declare class UndecryptableCredentialError extends Error {
|
|
15
|
+
constructor(cause?: unknown);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt a plaintext secret for storage.
|
|
19
|
+
*
|
|
20
|
+
* Format: `base64url(iv):base64url(authTag):base64url(ciphertext)` (12B IV).
|
|
21
|
+
* Uses `secret` from `ShelfConfig.secret` — throws if missing so misconfiguration
|
|
22
|
+
* fails loudly like `scratchDir` does in `capture/orchestrator.ts`.
|
|
23
|
+
*/
|
|
24
|
+
declare function encrypt(input: SecretInput, plaintext: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Decrypt a value produced by {@link encrypt}, trying the current key first and
|
|
27
|
+
* then the previous one (the rotation fallback).
|
|
28
|
+
*
|
|
29
|
+
* @param input - Server secret, or the current/previous pair during a rotation.
|
|
30
|
+
* @param ciphertext - Encrypted payload in `iv:tag:ciphertext` (base64url) format.
|
|
31
|
+
* @returns The original plaintext secret.
|
|
32
|
+
* @throws If no secret is configured, the payload is malformed, or no key decrypts it.
|
|
33
|
+
*/
|
|
34
|
+
declare function decrypt(input: SecretInput, ciphertext: string): string;
|
|
35
|
+
/** Like {@link decrypt}, also reporting which key matched. */
|
|
36
|
+
declare function decryptWithKey(input: SecretInput, ciphertext: string): {
|
|
37
|
+
plaintext: string;
|
|
38
|
+
keyUsed: KeyUsed;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Decrypt a stored credential, mapping any key mismatch or corruption to
|
|
42
|
+
* {@link UndecryptableCredentialError} so callers can fail loudly with a
|
|
43
|
+
* rotation hint. A missing secret keeps its own configuration error.
|
|
44
|
+
*/
|
|
45
|
+
declare function decryptCredential(input: SecretInput, ciphertext: string): string;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { KeyUsed, SecretInput, SecretKeys, UndecryptableCredentialError, decrypt, decryptCredential, decryptWithKey, encrypt };
|
|
48
|
+
//# sourceMappingURL=encrypt.d.mts.map
|
package/dist/utils/encrypt.mjs
CHANGED
|
@@ -3,14 +3,15 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
5
5
|
//#region src/utils/encrypt.ts
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
/** Thrown when a stored credential cannot be decrypted with any configured key. */
|
|
7
|
+
var UndecryptableCredentialError = class extends Error {
|
|
8
|
+
constructor(cause) {
|
|
9
|
+
super("Stored credential cannot be decrypted: check that SECRET (and SECRET_PREVIOUS after a rotation) match the key it was encrypted with", { cause });
|
|
10
|
+
this.name = "UndecryptableCredentialError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function currentOf(secret) {
|
|
14
|
+
return typeof secret === "object" ? secret.current : secret;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Encrypt a plaintext secret for storage.
|
|
@@ -19,7 +20,8 @@ function deriveKey(secret) {
|
|
|
19
20
|
* Uses `secret` from `ShelfConfig.secret` — throws if missing so misconfiguration
|
|
20
21
|
* fails loudly like `scratchDir` does in `capture/orchestrator.ts`.
|
|
21
22
|
*/
|
|
22
|
-
function encrypt(
|
|
23
|
+
function encrypt(input, plaintext) {
|
|
24
|
+
const secret = currentOf(input);
|
|
23
25
|
if (!secret) throw new Error("Cannot encrypt: ShelfConfig.secret is not configured");
|
|
24
26
|
const key = deriveKey(secret);
|
|
25
27
|
const iv = randomBytes(12);
|
|
@@ -29,10 +31,53 @@ function encrypt(secret, plaintext) {
|
|
|
29
31
|
return `${iv.toString("base64url")}:${tag.toString("base64url")}:${enc.toString("base64url")}`;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
|
-
* Decrypt a value produced by
|
|
34
|
+
* Decrypt a value produced by {@link encrypt}, trying the current key first and
|
|
35
|
+
* then the previous one (the rotation fallback).
|
|
36
|
+
*
|
|
37
|
+
* @param input - Server secret, or the current/previous pair during a rotation.
|
|
38
|
+
* @param ciphertext - Encrypted payload in `iv:tag:ciphertext` (base64url) format.
|
|
39
|
+
* @returns The original plaintext secret.
|
|
40
|
+
* @throws If no secret is configured, the payload is malformed, or no key decrypts it.
|
|
33
41
|
*/
|
|
34
|
-
function decrypt(
|
|
35
|
-
|
|
42
|
+
function decrypt(input, ciphertext) {
|
|
43
|
+
return decryptWithKey(input, ciphertext).plaintext;
|
|
44
|
+
}
|
|
45
|
+
/** Like {@link decrypt}, also reporting which key matched. */
|
|
46
|
+
function decryptWithKey(input, ciphertext) {
|
|
47
|
+
const current = currentOf(input);
|
|
48
|
+
if (!current) throw new Error("Cannot decrypt: ShelfConfig.secret is not configured");
|
|
49
|
+
try {
|
|
50
|
+
return {
|
|
51
|
+
plaintext: decryptWith(current, ciphertext),
|
|
52
|
+
keyUsed: "current"
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const previous = typeof input === "object" ? input.previous : void 0;
|
|
56
|
+
if (!previous) throw error;
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
plaintext: decryptWith(previous, ciphertext),
|
|
60
|
+
keyUsed: "previous"
|
|
61
|
+
};
|
|
62
|
+
} catch {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decrypt a stored credential, mapping any key mismatch or corruption to
|
|
69
|
+
* {@link UndecryptableCredentialError} so callers can fail loudly with a
|
|
70
|
+
* rotation hint. A missing secret keeps its own configuration error.
|
|
71
|
+
*/
|
|
72
|
+
function decryptCredential(input, ciphertext) {
|
|
73
|
+
if (!currentOf(input)) return decrypt(input, ciphertext);
|
|
74
|
+
try {
|
|
75
|
+
return decrypt(input, ciphertext);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new UndecryptableCredentialError(error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function decryptWith(secret, ciphertext) {
|
|
36
81
|
const [ivB64, tagB64, encB64] = ciphertext.split(":");
|
|
37
82
|
if (!ivB64 || !tagB64 || !encB64) throw new Error("Invalid encrypted payload format");
|
|
38
83
|
const key = deriveKey(secret);
|
|
@@ -43,7 +88,16 @@ function decrypt(secret, ciphertext) {
|
|
|
43
88
|
decipher.setAuthTag(tag);
|
|
44
89
|
return Buffer.concat([decipher.update(enc), decipher.final()]).toString("utf8");
|
|
45
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Derive a 32-byte AES-256 key from the server secret.
|
|
93
|
+
*
|
|
94
|
+
* Reuses the existing `sha256` pattern (`utils/hash.ts`) so key derivation
|
|
95
|
+
* is consistent with token hashing and session HMAC.
|
|
96
|
+
*/
|
|
97
|
+
function deriveKey(secret) {
|
|
98
|
+
return createHash("sha256").update(secret).digest();
|
|
99
|
+
}
|
|
46
100
|
//#endregion
|
|
47
|
-
export { decrypt, encrypt };
|
|
101
|
+
export { UndecryptableCredentialError, decrypt, decryptCredential, decryptWithKey, encrypt };
|
|
48
102
|
|
|
49
103
|
//# sourceMappingURL=encrypt.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt.mjs","names":[],"sources":["../../src/utils/encrypt.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from \"node:crypto\";\n\n
|
|
1
|
+
{"version":3,"file":"encrypt.mjs","names":[],"sources":["../../src/utils/encrypt.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from \"node:crypto\";\n\n/** Current server secret plus the one it replaced, kept only to decrypt older rows. */\nexport interface SecretKeys {\n current: string;\n previous?: string;\n}\n\n/** What credential models accept: a bare secret, a rotation pair, or nothing. */\nexport type SecretInput = string | SecretKeys | undefined;\n\n/** Which key decrypted a payload. */\nexport type KeyUsed = \"current\" | \"previous\";\n\n/** Thrown when a stored credential cannot be decrypted with any configured key. */\nexport class UndecryptableCredentialError extends Error {\n constructor(cause?: unknown) {\n super(\n \"Stored credential cannot be decrypted: check that SECRET (and SECRET_PREVIOUS after a rotation) match the key it was encrypted with\",\n { cause },\n );\n this.name = \"UndecryptableCredentialError\";\n }\n}\n\nfunction currentOf(secret: SecretInput): string | undefined {\n return typeof secret === \"object\" ? secret.current : secret;\n}\n\n/**\n * Encrypt a plaintext secret for storage.\n *\n * Format: `base64url(iv):base64url(authTag):base64url(ciphertext)` (12B IV).\n * Uses `secret` from `ShelfConfig.secret` — throws if missing so misconfiguration\n * fails loudly like `scratchDir` does in `capture/orchestrator.ts`.\n */\nexport function encrypt(input: SecretInput, plaintext: string): string {\n const secret = currentOf(input);\n if (!secret) {\n throw new Error(\"Cannot encrypt: ShelfConfig.secret is not configured\");\n }\n const key = deriveKey(secret);\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n const enc = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `${iv.toString(\"base64url\")}:${tag.toString(\"base64url\")}:${enc.toString(\"base64url\")}`;\n}\n\n/**\n * Decrypt a value produced by {@link encrypt}, trying the current key first and\n * then the previous one (the rotation fallback).\n *\n * @param input - Server secret, or the current/previous pair during a rotation.\n * @param ciphertext - Encrypted payload in `iv:tag:ciphertext` (base64url) format.\n * @returns The original plaintext secret.\n * @throws If no secret is configured, the payload is malformed, or no key decrypts it.\n */\nexport function decrypt(input: SecretInput, ciphertext: string): string {\n return decryptWithKey(input, ciphertext).plaintext;\n}\n\n/** Like {@link decrypt}, also reporting which key matched. */\nexport function decryptWithKey(\n input: SecretInput,\n ciphertext: string,\n): { plaintext: string; keyUsed: KeyUsed } {\n const current = currentOf(input);\n if (!current) {\n throw new Error(\"Cannot decrypt: ShelfConfig.secret is not configured\");\n }\n try {\n return { plaintext: decryptWith(current, ciphertext), keyUsed: \"current\" };\n } catch (error) {\n const previous = typeof input === \"object\" ? input.previous : undefined;\n if (!previous) {\n throw error;\n }\n try {\n return { plaintext: decryptWith(previous, ciphertext), keyUsed: \"previous\" };\n } catch {\n throw error;\n }\n }\n}\n\n/**\n * Decrypt a stored credential, mapping any key mismatch or corruption to\n * {@link UndecryptableCredentialError} so callers can fail loudly with a\n * rotation hint. A missing secret keeps its own configuration error.\n */\nexport function decryptCredential(input: SecretInput, ciphertext: string): string {\n if (!currentOf(input)) {\n return decrypt(input, ciphertext);\n }\n try {\n return decrypt(input, ciphertext);\n } catch (error) {\n throw new UndecryptableCredentialError(error);\n }\n}\n\nfunction decryptWith(secret: string, ciphertext: string): string {\n const [ivB64, tagB64, encB64] = ciphertext.split(\":\");\n if (!ivB64 || !tagB64 || !encB64) {\n throw new Error(\"Invalid encrypted payload format\");\n }\n const key = deriveKey(secret);\n const iv = Buffer.from(ivB64, \"base64url\");\n const tag = Buffer.from(tagB64, \"base64url\");\n const enc = Buffer.from(encB64, \"base64url\");\n const decipher = createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAuthTag(tag);\n const dec = Buffer.concat([decipher.update(enc), decipher.final()]);\n return dec.toString(\"utf8\");\n}\n\n/**\n * Derive a 32-byte AES-256 key from the server secret.\n *\n * Reuses the existing `sha256` pattern (`utils/hash.ts`) so key derivation\n * is consistent with token hashing and session HMAC.\n */\nfunction deriveKey(secret: string): Buffer {\n return createHash(\"sha256\").update(secret).digest();\n}\n"],"mappings":";;;;;;AAeA,IAAa,+BAAb,cAAkD,MAAM;CACtD,YAAY,OAAiB;EAC3B,MACE,uIACA,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;AAEA,SAAS,UAAU,QAAyC;CAC1D,OAAO,OAAO,WAAW,WAAW,OAAO,UAAU;AACvD;;;;;;;;AASA,SAAgB,QAAQ,OAAoB,WAA2B;CACrE,MAAM,SAAS,UAAU,KAAK;CAC9B,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,sDAAsD;CAExE,MAAM,MAAM,UAAU,MAAM;CAC5B,MAAM,KAAK,YAAY,EAAE;CACzB,MAAM,SAAS,eAAe,eAAe,KAAK,EAAE;CACpD,MAAM,MAAM,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;CAC5E,MAAM,MAAM,OAAO,WAAW;CAC9B,OAAO,GAAG,GAAG,SAAS,WAAW,EAAE,GAAG,IAAI,SAAS,WAAW,EAAE,GAAG,IAAI,SAAS,WAAW;AAC7F;;;;;;;;;;AAWA,SAAgB,QAAQ,OAAoB,YAA4B;CACtE,OAAO,eAAe,OAAO,UAAU,CAAC,CAAC;AAC3C;;AAGA,SAAgB,eACd,OACA,YACyC;CACzC,MAAM,UAAU,UAAU,KAAK;CAC/B,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,sDAAsD;CAExE,IAAI;EACF,OAAO;GAAE,WAAW,YAAY,SAAS,UAAU;GAAG,SAAS;EAAU;CAC3E,SAAS,OAAO;EACd,MAAM,WAAW,OAAO,UAAU,WAAW,MAAM,WAAW,KAAA;EAC9D,IAAI,CAAC,UACH,MAAM;EAER,IAAI;GACF,OAAO;IAAE,WAAW,YAAY,UAAU,UAAU;IAAG,SAAS;GAAW;EAC7E,QAAQ;GACN,MAAM;EACR;CACF;AACF;;;;;;AAOA,SAAgB,kBAAkB,OAAoB,YAA4B;CAChF,IAAI,CAAC,UAAU,KAAK,GAClB,OAAO,QAAQ,OAAO,UAAU;CAElC,IAAI;EACF,OAAO,QAAQ,OAAO,UAAU;CAClC,SAAS,OAAO;EACd,MAAM,IAAI,6BAA6B,KAAK;CAC9C;AACF;AAEA,SAAS,YAAY,QAAgB,YAA4B;CAC/D,MAAM,CAAC,OAAO,QAAQ,UAAU,WAAW,MAAM,GAAG;CACpD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,QACxB,MAAM,IAAI,MAAM,kCAAkC;CAEpD,MAAM,MAAM,UAAU,MAAM;CAC5B,MAAM,KAAK,OAAO,KAAK,OAAO,WAAW;CACzC,MAAM,MAAM,OAAO,KAAK,QAAQ,WAAW;CAC3C,MAAM,MAAM,OAAO,KAAK,QAAQ,WAAW;CAC3C,MAAM,WAAW,iBAAiB,eAAe,KAAK,EAAE;CACxD,SAAS,WAAW,GAAG;CAEvB,OADY,OAAO,OAAO,CAAC,SAAS,OAAO,GAAG,GAAG,SAAS,MAAM,CAAC,CACxD,CAAC,CAAC,SAAS,MAAM;AAC5B;;;;;;;AAQA,SAAS,UAAU,QAAwB;CACzC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,OAAO;AACpD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
//#region src/utils/hash.d.ts
|
|
4
|
+
/** Hash a value with SHA-256, returning lowercase hex. */
|
|
5
|
+
declare function sha256(value: string): string;
|
|
6
|
+
/** Sign a value with HMAC-SHA-256, returning lowercase hex. */
|
|
7
|
+
declare function hmacSha256(secret: string, value: string): string;
|
|
8
|
+
/** Generate a prefixed random token and its SHA-256 hash. */
|
|
9
|
+
declare function randomToken(prefix: string): {
|
|
10
|
+
value: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
};
|
|
13
|
+
/** Compare two strings in constant time to avoid timing leaks. */
|
|
14
|
+
declare function timingSafeEqualString(a: string, b: string): boolean;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { hmacSha256, randomToken, sha256, timingSafeEqualString };
|
|
17
|
+
//# sourceMappingURL=hash.d.mts.map
|
package/dist/utils/hash.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
4
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
5
|
//#region src/utils/hash.ts
|
|
6
6
|
/** Hash a value with SHA-256, returning lowercase hex. */
|
|
7
7
|
function sha256(value) {
|
|
@@ -19,7 +19,13 @@ function randomToken(prefix) {
|
|
|
19
19
|
hash: sha256(value)
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
/** Compare two strings in constant time to avoid timing leaks. */
|
|
23
|
+
function timingSafeEqualString(a, b) {
|
|
24
|
+
const ab = Buffer.from(a);
|
|
25
|
+
const bb = Buffer.from(b);
|
|
26
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
27
|
+
}
|
|
22
28
|
//#endregion
|
|
23
|
-
export { hmacSha256, randomToken, sha256 };
|
|
29
|
+
export { hmacSha256, randomToken, sha256, timingSafeEqualString };
|
|
24
30
|
|
|
25
31
|
//# sourceMappingURL=hash.mjs.map
|
package/dist/utils/hash.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.mjs","names":[],"sources":["../../src/utils/hash.ts"],"sourcesContent":["import { createHash, createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/** Hash a value with SHA-256, returning lowercase hex. */\nexport function sha256(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\n/** Sign a value with HMAC-SHA-256, returning lowercase hex. */\nexport function hmacSha256(secret: string, value: string): string {\n return createHmac(\"sha256\", secret).update(value).digest(\"hex\");\n}\n\n/** Generate a prefixed random token and its SHA-256 hash. */\nexport function randomToken(prefix: string): { value: string; hash: string } {\n const value = `${prefix}${randomBytes(24).toString(\"base64url\")}`;\n return { value, hash: sha256(value) };\n}\n\n/** Compare two strings in constant time to avoid timing leaks. */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n return ab.length === bb.length && timingSafeEqual(ab, bb);\n}\n"],"mappings":";;;;;;AAGA,SAAgB,OAAO,OAAuB;CAC5C,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AACxD;;AAGA,SAAgB,WAAW,QAAgB,OAAuB;CAChE,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AAChE;;AAGA,SAAgB,YAAY,QAAiD;CAC3E,MAAM,QAAQ,GAAG,SAAS,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;CAC9D,OAAO;EAAE;EAAO,MAAM,OAAO,KAAK;CAAE;AACtC"}
|
|
1
|
+
{"version":3,"file":"hash.mjs","names":[],"sources":["../../src/utils/hash.ts"],"sourcesContent":["import { createHash, createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/** Hash a value with SHA-256, returning lowercase hex. */\nexport function sha256(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\n/** Sign a value with HMAC-SHA-256, returning lowercase hex. */\nexport function hmacSha256(secret: string, value: string): string {\n return createHmac(\"sha256\", secret).update(value).digest(\"hex\");\n}\n\n/** Generate a prefixed random token and its SHA-256 hash. */\nexport function randomToken(prefix: string): { value: string; hash: string } {\n const value = `${prefix}${randomBytes(24).toString(\"base64url\")}`;\n return { value, hash: sha256(value) };\n}\n\n/** Compare two strings in constant time to avoid timing leaks. */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n return ab.length === bb.length && timingSafeEqual(ab, bb);\n}\n"],"mappings":";;;;;;AAGA,SAAgB,OAAO,OAAuB;CAC5C,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AACxD;;AAGA,SAAgB,WAAW,QAAgB,OAAuB;CAChE,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AAChE;;AAGA,SAAgB,YAAY,QAAiD;CAC3E,MAAM,QAAQ,GAAG,SAAS,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;CAC9D,OAAO;EAAE;EAAO,MAAM,OAAO,KAAK;CAAE;AACtC;;AAGA,SAAgB,sBAAsB,GAAW,GAAoB;CACnE,MAAM,KAAK,OAAO,KAAK,CAAC;CACxB,MAAM,KAAK,OAAO,KAAK,CAAC;CACxB,OAAO,GAAG,WAAW,GAAG,UAAU,gBAAgB,IAAI,EAAE;AAC1D"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { Logger } from "../logger.mjs";
|
|
4
|
+
//#region src/utils/http.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Fetch JSON with timeout, retries, and structured errors.
|
|
7
|
+
*
|
|
8
|
+
* Retries network failures, 429, and 502/503/504 (honoring `Retry-After`);
|
|
9
|
+
* every other status throws {@link HttpError} immediately.
|
|
10
|
+
*
|
|
11
|
+
* @param url - Absolute request URL.
|
|
12
|
+
* @param options - Method, headers, JSON body, timeout, retries, logger.
|
|
13
|
+
* @returns The parsed JSON response body.
|
|
14
|
+
*/
|
|
15
|
+
declare function httpJson<T>(url: string, options?: HttpRequestOptions): Promise<T>;
|
|
16
|
+
/**
|
|
17
|
+
* POST/PUT plain payloads (e.g. Slack/Teams webhook cards) with the same
|
|
18
|
+
* timeout, retry, and `Retry-After` semantics as {@link httpJson}.
|
|
19
|
+
*
|
|
20
|
+
* @param url - Absolute request URL.
|
|
21
|
+
* @param options - Method, headers, text body, timeout, retries, logger.
|
|
22
|
+
* @returns The raw response body text.
|
|
23
|
+
*/
|
|
24
|
+
declare function httpText(url: string, options?: HttpTextOptions): Promise<string>;
|
|
25
|
+
/** Failed HTTP response: status code plus a snippet of the response body. */
|
|
26
|
+
declare class HttpError extends Error {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly body: string;
|
|
29
|
+
readonly headers: Headers;
|
|
30
|
+
constructor(status: number, body: string, headers?: Headers);
|
|
31
|
+
}
|
|
32
|
+
/** Options for {@link httpJson}. */
|
|
33
|
+
interface HttpRequestOptions {
|
|
34
|
+
method?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** Body serialized as JSON with a `content-type` header. */
|
|
37
|
+
json?: unknown;
|
|
38
|
+
/** Per-attempt timeout in milliseconds (default 30s). */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** Total attempts including the first (default 3). */
|
|
41
|
+
retries?: number;
|
|
42
|
+
logger?: Logger;
|
|
43
|
+
}
|
|
44
|
+
/** Options for {@link httpText}. */
|
|
45
|
+
interface HttpTextOptions {
|
|
46
|
+
method?: string;
|
|
47
|
+
headers?: Record<string, string>;
|
|
48
|
+
/** Body serialized as JSON with a `content-type` header. */
|
|
49
|
+
json?: unknown;
|
|
50
|
+
/** Per-attempt timeout in milliseconds (default 30s). */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
/** Total attempts including the first (default 3). */
|
|
53
|
+
retries?: number;
|
|
54
|
+
logger?: Logger;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { HttpError, HttpRequestOptions, HttpTextOptions, httpJson, httpText };
|
|
58
|
+
//# sourceMappingURL=http.d.mts.map
|