@storyshelf/core 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -32
- package/dist/adapter/capture-queue.d.mts +44 -2
- package/dist/adapter/capture-runner.d.mts +127 -10
- package/dist/adapter/capture-runner.mjs +5 -12
- package/dist/adapter/database.d.mts +27 -8
- package/dist/adapter/database.mjs.map +1 -1
- package/dist/adapter/design.d.mts +85 -0
- package/dist/adapter/design.mjs +28 -0
- package/dist/adapter/design.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +3 -10
- package/dist/adapter/notifier.d.mts +6 -3
- package/dist/adapter/notifier.mjs +5 -1
- package/dist/adapter/oauth.d.mts +52 -0
- package/dist/adapter/setup.d.mts +4 -5
- package/dist/adapter/setup.mjs +2 -1
- package/dist/adapter/setup.mjs.map +1 -1
- package/dist/adapter/webhook-events.mjs +1 -1
- package/dist/adapters/capture-target-offers.d.mts +28 -0
- package/dist/adapters/capture-target-offers.mjs +58 -0
- package/dist/adapters/capture-target-offers.mjs.map +1 -0
- package/dist/adapters/capture-target.d.mts +71 -0
- package/dist/adapters/capture-target.mjs +76 -0
- package/dist/adapters/capture-target.mjs.map +1 -0
- package/dist/adapters/notifier/build-comment.d.mts +24 -0
- package/dist/adapters/notifier/build-comment.mjs +24 -0
- package/dist/adapters/notifier/build-comment.mjs.map +1 -0
- package/dist/adapters/notifier/build-state.d.mts +99 -0
- package/dist/adapters/notifier/build-state.mjs +218 -0
- package/dist/adapters/notifier/build-state.mjs.map +1 -0
- package/dist/adapters/notifier/events.d.mts +4 -2
- package/dist/adapters/notifier/events.mjs +31 -18
- package/dist/adapters/notifier/events.mjs.map +1 -1
- package/dist/adapters/notifier/provider.d.mts +53 -7
- package/dist/adapters/notifier/provider.mjs +12 -0
- package/dist/adapters/notifier/provider.mjs.map +1 -0
- package/dist/adapters/notifier/state.d.mts +24 -0
- package/dist/adapters/notifier/state.mjs +66 -0
- package/dist/adapters/notifier/state.mjs.map +1 -0
- package/dist/adapters/notifier/types.d.mts +51 -2
- package/dist/adapters/remote-browser.d.mts +87 -0
- package/dist/adapters/remote-browser.mjs +103 -0
- package/dist/adapters/remote-browser.mjs.map +1 -0
- package/dist/adapters/render-runs.d.mts +25 -0
- package/dist/adapters/render-runs.mjs +41 -0
- package/dist/adapters/render-runs.mjs.map +1 -0
- package/dist/adapters/setup-runner.mjs +2 -4
- package/dist/adapters/setup-runner.mjs.map +1 -1
- package/dist/adapters/sources.d.mts +12 -0
- package/dist/adapters/sources.mjs +20 -0
- package/dist/adapters/sources.mjs.map +1 -0
- package/dist/adapters/validate.mjs +25 -18
- package/dist/adapters/validate.mjs.map +1 -1
- package/dist/ai.d.mts +105 -0
- package/dist/ai.mjs +19 -0
- package/dist/ai.mjs.map +1 -0
- package/dist/auth.d.mts +8 -1
- package/dist/auth.mjs +14 -1
- package/dist/auth.mjs.map +1 -0
- package/dist/capture/a11y/axe-source.d.mts +18 -0
- package/dist/capture/a11y/axe-source.mjs +37 -0
- package/dist/capture/a11y/axe-source.mjs.map +1 -0
- package/dist/capture/a11y/in-page.d.mts +27 -0
- package/dist/capture/a11y/in-page.mjs +60 -0
- package/dist/capture/a11y/in-page.mjs.map +1 -0
- package/dist/capture/a11y/map.d.mts +24 -0
- package/dist/capture/a11y/map.mjs +48 -0
- package/dist/capture/a11y/map.mjs.map +1 -0
- package/dist/capture/a11y/options.d.mts +23 -0
- package/dist/capture/a11y/options.mjs +75 -0
- package/dist/capture/a11y/options.mjs.map +1 -0
- package/dist/capture/adapter.d.mts +41 -3
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/affected.d.mts +2 -0
- package/dist/capture/affected.mjs +13 -4
- package/dist/capture/affected.mjs.map +1 -1
- package/dist/capture/attempt-logger.mjs +84 -0
- package/dist/capture/attempt-logger.mjs.map +1 -0
- package/dist/capture/browsers.d.mts +35 -0
- package/dist/capture/browsers.mjs +67 -0
- package/dist/capture/browsers.mjs.map +1 -0
- package/dist/capture/build-state/deliver.d.mts +32 -0
- package/dist/capture/build-state/deliver.mjs +118 -0
- package/dist/capture/build-state/deliver.mjs.map +1 -0
- package/dist/capture/build-state/index.d.mts +8 -0
- package/dist/capture/build-state/notify-job.d.mts +56 -0
- package/dist/capture/build-state/notify-job.mjs +133 -0
- package/dist/capture/build-state/notify-job.mjs.map +1 -0
- package/dist/capture/build-state/publisher.d.mts +23 -0
- package/dist/capture/build-state/publisher.mjs +107 -0
- package/dist/capture/build-state/publisher.mjs.map +1 -0
- package/dist/capture/build-state/resume.d.mts +9 -0
- package/dist/capture/build-state/resume.mjs +47 -0
- package/dist/capture/build-state/resume.mjs.map +1 -0
- package/dist/capture/build-state/types.d.mts +49 -0
- package/dist/capture/catalog/catalogue.d.mts +22 -0
- package/dist/capture/catalog/catalogue.mjs +53 -0
- package/dist/capture/catalog/catalogue.mjs.map +1 -0
- package/dist/capture/catalog/components.d.mts +23 -0
- package/dist/capture/catalog/components.mjs +55 -0
- package/dist/capture/catalog/components.mjs.map +1 -0
- package/dist/capture/catalog/history.d.mts +30 -0
- package/dist/capture/catalog/history.mjs +51 -0
- package/dist/capture/catalog/history.mjs.map +1 -0
- package/dist/capture/catalog/hook.d.mts +14 -0
- package/dist/capture/catalog/hook.mjs +19 -0
- package/dist/capture/catalog/hook.mjs.map +1 -0
- package/dist/capture/catalog/index.d.mts +11 -0
- package/dist/capture/catalog/load.d.mts +26 -0
- package/dist/capture/catalog/load.mjs +49 -0
- package/dist/capture/catalog/load.mjs.map +1 -0
- package/dist/capture/catalog/parse.d.mts +15 -0
- package/dist/capture/catalog/parse.mjs +56 -0
- package/dist/capture/catalog/parse.mjs.map +1 -0
- package/dist/capture/catalog/read.d.mts +12 -0
- package/dist/capture/catalog/read.mjs +33 -0
- package/dist/capture/catalog/read.mjs.map +1 -0
- package/dist/capture/catalog/types.d.mts +33 -0
- package/dist/capture/consumer/capture-handler.d.mts +58 -0
- package/dist/capture/consumer/capture-handler.mjs +118 -0
- package/dist/capture/consumer/capture-handler.mjs.map +1 -0
- package/dist/capture/consumer/idle.d.mts +17 -0
- package/dist/capture/consumer/idle.mjs +25 -0
- package/dist/capture/consumer/idle.mjs.map +1 -0
- package/dist/capture/consumer/index.d.mts +13 -0
- package/dist/capture/consumer/index.mjs +139 -0
- package/dist/capture/consumer/index.mjs.map +1 -0
- package/dist/capture/consumer/loop.mjs +110 -0
- package/dist/capture/consumer/loop.mjs.map +1 -0
- package/dist/capture/consumer/metrics.d.mts +41 -0
- package/dist/capture/consumer/metrics.mjs +38 -0
- package/dist/capture/consumer/metrics.mjs.map +1 -0
- package/dist/capture/consumer/retry.d.mts +33 -0
- package/dist/capture/consumer/retry.mjs +34 -0
- package/dist/capture/consumer/retry.mjs.map +1 -0
- package/dist/capture/consumer/run-job.mjs +195 -0
- package/dist/capture/consumer/run-job.mjs.map +1 -0
- package/dist/capture/consumer/startup.mjs +38 -0
- package/dist/capture/consumer/startup.mjs.map +1 -0
- package/dist/capture/consumer/types.d.mts +97 -0
- package/dist/capture/crash-failure.d.mts +25 -0
- package/dist/capture/crash-failure.mjs +70 -0
- package/dist/capture/crash-failure.mjs.map +1 -0
- package/dist/capture/db-queue/cadence.d.mts +32 -0
- package/dist/capture/db-queue/cadence.mjs +38 -0
- package/dist/capture/db-queue/cadence.mjs.map +1 -0
- package/dist/capture/db-queue/claim.d.mts +9 -0
- package/dist/capture/db-queue/claim.mjs +132 -0
- package/dist/capture/db-queue/claim.mjs.map +1 -0
- package/dist/capture/db-queue/clock.mjs +12 -0
- package/dist/capture/db-queue/clock.mjs.map +1 -0
- package/dist/capture/db-queue/codec.mjs +76 -0
- package/dist/capture/db-queue/codec.mjs.map +1 -0
- package/dist/capture/db-queue/index.d.mts +20 -0
- package/dist/capture/db-queue/index.mjs +89 -0
- package/dist/capture/db-queue/index.mjs.map +1 -0
- package/dist/capture/db-queue/keeper.d.mts +36 -0
- package/dist/capture/db-queue/keeper.mjs +92 -0
- package/dist/capture/db-queue/keeper.mjs.map +1 -0
- package/dist/capture/db-queue/lease.mjs +46 -0
- package/dist/capture/db-queue/lease.mjs.map +1 -0
- package/dist/capture/db-queue/lifecycle.mjs +36 -0
- package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
- package/dist/capture/db-queue/operations.mjs +94 -0
- package/dist/capture/db-queue/operations.mjs.map +1 -0
- package/dist/capture/db-queue/poll-loop.d.mts +30 -0
- package/dist/capture/db-queue/poll-loop.mjs +89 -0
- package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
- package/dist/capture/db-queue/poll.d.mts +10 -0
- package/dist/capture/db-queue/poll.mjs +69 -0
- package/dist/capture/db-queue/poll.mjs.map +1 -0
- package/dist/capture/db-queue/timers.d.mts +12 -0
- package/dist/capture/db-queue/timers.mjs +37 -0
- package/dist/capture/db-queue/timers.mjs.map +1 -0
- package/dist/capture/db-queue/types.d.mts +83 -0
- package/dist/capture/db-queue/types.mjs +12 -0
- package/dist/capture/db-queue/types.mjs.map +1 -0
- package/dist/capture/db-queue/wake.mjs +48 -0
- package/dist/capture/db-queue/wake.mjs.map +1 -0
- package/dist/capture/design.mjs +39 -0
- package/dist/capture/design.mjs.map +1 -0
- package/dist/capture/diff-record.mjs +23 -3
- package/dist/capture/diff-record.mjs.map +1 -1
- package/dist/capture/dispatch.d.mts +12 -7
- package/dist/capture/dispatch.mjs +86 -107
- package/dist/capture/dispatch.mjs.map +1 -1
- package/dist/capture/enrich.mjs +60 -0
- package/dist/capture/enrich.mjs.map +1 -0
- package/dist/capture/failure-sets.mjs +22 -0
- package/dist/capture/failure-sets.mjs.map +1 -0
- package/dist/capture/fingerprint.d.mts +42 -0
- package/dist/capture/fingerprint.mjs +83 -0
- package/dist/capture/fingerprint.mjs.map +1 -0
- package/dist/capture/forked/channel.d.mts +25 -0
- package/dist/capture/forked/child.d.mts +34 -0
- package/dist/capture/forked/child.mjs +202 -0
- package/dist/capture/forked/child.mjs.map +1 -0
- package/dist/capture/forked/crash-tracker.mjs +23 -0
- package/dist/capture/forked/crash-tracker.mjs.map +1 -0
- package/dist/capture/forked/decision.d.mts +26 -0
- package/dist/capture/forked/decision.mjs +27 -0
- package/dist/capture/forked/decision.mjs.map +1 -0
- package/dist/capture/forked/exit.mjs +12 -0
- package/dist/capture/forked/exit.mjs.map +1 -0
- package/dist/capture/forked/fork.d.mts +31 -0
- package/dist/capture/forked/fork.mjs +93 -0
- package/dist/capture/forked/fork.mjs.map +1 -0
- package/dist/capture/forked/forked-capture.d.mts +40 -0
- package/dist/capture/forked/forked-capture.mjs +240 -0
- package/dist/capture/forked/forked-capture.mjs.map +1 -0
- package/dist/capture/forked/lease-book.mjs +46 -0
- package/dist/capture/forked/lease-book.mjs.map +1 -0
- package/dist/capture/forked/protocol.d.mts +38 -0
- package/dist/capture/forked/protocol.mjs +25 -0
- package/dist/capture/forked/protocol.mjs.map +1 -0
- package/dist/capture/forked/runs.mjs +75 -0
- package/dist/capture/forked/runs.mjs.map +1 -0
- package/dist/capture/isolate.mjs +141 -0
- package/dist/capture/isolate.mjs.map +1 -0
- package/dist/capture/job-lease.d.mts +28 -0
- package/dist/capture/job-lease.mjs +57 -0
- package/dist/capture/job-lease.mjs.map +1 -0
- package/dist/capture/matrix.mjs +298 -0
- package/dist/capture/matrix.mjs.map +1 -0
- package/dist/capture/modes.d.mts +25 -0
- package/dist/capture/modes.mjs +107 -0
- package/dist/capture/modes.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +37 -5
- package/dist/capture/orchestrator.mjs +118 -107
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +9 -2
- package/dist/capture/pipeline.mjs +250 -45
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/progress.d.mts +11 -0
- package/dist/capture/progress.mjs +28 -0
- package/dist/capture/progress.mjs.map +1 -0
- package/dist/capture/publish/accept.d.mts +39 -0
- package/dist/capture/publish/accept.mjs +28 -0
- package/dist/capture/publish/accept.mjs.map +1 -0
- package/dist/capture/publish/complete.d.mts +46 -0
- package/dist/capture/publish/complete.mjs +64 -0
- package/dist/capture/publish/complete.mjs.map +1 -0
- package/dist/capture/publish/extract.d.mts +20 -0
- package/dist/capture/publish/extract.mjs +119 -0
- package/dist/capture/publish/extract.mjs.map +1 -0
- package/dist/capture/publish/handler.d.mts +28 -0
- package/dist/capture/publish/handler.mjs +62 -0
- package/dist/capture/publish/handler.mjs.map +1 -0
- package/dist/capture/publish/index.d.mts +10 -0
- package/dist/capture/publish/limits.d.mts +37 -0
- package/dist/capture/publish/limits.mjs +39 -0
- package/dist/capture/publish/limits.mjs.map +1 -0
- package/dist/capture/publish/published.d.mts +15 -0
- package/dist/capture/publish/published.mjs +18 -0
- package/dist/capture/publish/published.mjs.map +1 -0
- package/dist/capture/publish/run.d.mts +33 -0
- package/dist/capture/publish/run.mjs +30 -0
- package/dist/capture/publish/run.mjs.map +1 -0
- package/dist/capture/rediff.d.mts +1 -1
- package/dist/capture/rediff.mjs +4 -3
- package/dist/capture/rediff.mjs.map +1 -1
- package/dist/capture/render.mjs +53 -0
- package/dist/capture/render.mjs.map +1 -0
- package/dist/capture/scratch.d.mts +44 -0
- package/dist/capture/scratch.mjs +138 -0
- package/dist/capture/scratch.mjs.map +1 -0
- package/dist/capture/shard-plan.mjs +128 -0
- package/dist/capture/shard-plan.mjs.map +1 -0
- package/dist/capture/shards/context.mjs +51 -0
- package/dist/capture/shards/context.mjs.map +1 -0
- package/dist/capture/shards/execute.mjs +174 -0
- package/dist/capture/shards/execute.mjs.map +1 -0
- package/dist/capture/shards/fan-out.mjs +126 -0
- package/dist/capture/shards/fan-out.mjs.map +1 -0
- package/dist/capture/shards/history.mjs +35 -0
- package/dist/capture/shards/history.mjs.map +1 -0
- package/dist/capture/shards/reduce.mjs +97 -0
- package/dist/capture/shards/reduce.mjs.map +1 -0
- package/dist/capture/sizing.mjs +1 -11
- package/dist/capture/sizing.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +2 -65
- package/dist/capture/skip-checks.mjs.map +1 -1
- package/dist/capture/statics.d.mts +7 -8
- package/dist/capture/statics.mjs +25 -71
- package/dist/capture/statics.mjs.map +1 -1
- package/dist/capture/storybook-urls.d.mts +40 -0
- package/dist/capture/storybook-urls.mjs +41 -0
- package/dist/capture/storybook-urls.mjs.map +1 -0
- package/dist/capture/storybook.d.mts +12 -4
- package/dist/capture/storybook.mjs +56 -16
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture/target.mjs +21 -0
- package/dist/capture/target.mjs.map +1 -0
- package/dist/capture/targets.d.mts +25 -0
- package/dist/capture/targets.mjs +32 -0
- package/dist/capture/targets.mjs.map +1 -0
- package/dist/capture/tests.d.mts +37 -0
- package/dist/capture/tests.mjs +126 -0
- package/dist/capture/tests.mjs.map +1 -0
- package/dist/capture/token-chunks.mjs +74 -0
- package/dist/capture/token-chunks.mjs.map +1 -0
- package/dist/capture.d.mts +60 -7
- package/dist/capture.mjs +48 -3
- package/dist/config.d.mts +121 -12
- package/dist/config.mjs +9 -6
- package/dist/config.mjs.map +1 -1
- package/dist/diff.d.mts +9 -3
- package/dist/diff.mjs +16 -3
- package/dist/diff.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/insights/budget.d.mts +20 -0
- package/dist/insights/budget.mjs +33 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/envelope.d.mts +59 -0
- package/dist/insights/envelope.mjs +60 -0
- package/dist/insights/envelope.mjs.map +1 -0
- package/dist/insights/evidence-health.d.mts +32 -0
- package/dist/insights/evidence-health.mjs +28 -0
- package/dist/insights/evidence-health.mjs.map +1 -0
- package/dist/insights/evidence.d.mts +55 -0
- package/dist/insights/evidence.mjs +64 -0
- package/dist/insights/evidence.mjs.map +1 -0
- package/dist/insights/hash.d.mts +18 -0
- package/dist/insights/hash.mjs +21 -0
- package/dist/insights/hash.mjs.map +1 -0
- package/dist/insights/limits.d.mts +24 -0
- package/dist/insights/limits.mjs +24 -0
- package/dist/insights/limits.mjs.map +1 -0
- package/dist/insights/prompts/v1.d.mts +13 -0
- package/dist/insights/prompts/v1.mjs +34 -0
- package/dist/insights/prompts/v1.mjs.map +1 -0
- package/dist/insights/redact.d.mts +9 -0
- package/dist/insights/redact.mjs +24 -0
- package/dist/insights/redact.mjs.map +1 -0
- package/dist/insights/text.mjs +44 -0
- package/dist/insights/text.mjs.map +1 -0
- package/dist/insights.d.mts +11 -0
- package/dist/insights.mjs +12 -0
- package/dist/logger.mjs +17 -1
- package/dist/logger.mjs.map +1 -1
- package/dist/models/ai-budget-alert.d.mts +23 -0
- package/dist/models/ai-budget-alert.mjs +38 -0
- package/dist/models/ai-budget-alert.mjs.map +1 -0
- package/dist/models/ai-usage.d.mts +40 -0
- package/dist/models/ai-usage.mjs +61 -0
- package/dist/models/ai-usage.mjs.map +1 -0
- package/dist/models/baseline-archive.d.mts +84 -0
- package/dist/models/baseline-archive.mjs +106 -0
- package/dist/models/baseline-archive.mjs.map +1 -0
- package/dist/models/baseline-export.d.mts +19 -0
- package/dist/models/baseline-export.mjs +136 -0
- package/dist/models/baseline-export.mjs.map +1 -0
- package/dist/models/baseline-import.mjs +118 -0
- package/dist/models/baseline-import.mjs.map +1 -0
- package/dist/models/baseline-transfer.d.mts +49 -0
- package/dist/models/baseline-transfer.mjs +121 -0
- package/dist/models/baseline-transfer.mjs.map +1 -0
- package/dist/models/baseline.d.mts +32 -5
- package/dist/models/baseline.mjs +65 -20
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build-component.d.mts +28 -0
- package/dist/models/build-component.mjs +55 -0
- package/dist/models/build-component.mjs.map +1 -0
- package/dist/models/build-review-status.d.mts +29 -0
- package/dist/models/build-review-status.mjs +37 -0
- package/dist/models/build-review-status.mjs.map +1 -0
- package/dist/models/build-review.d.mts +28 -0
- package/dist/models/build-review.mjs +54 -0
- package/dist/models/build-review.mjs.map +1 -0
- package/dist/models/build.d.mts +25 -3
- package/dist/models/build.mjs +50 -11
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/capture-attempt.d.mts +7 -1
- package/dist/models/capture-attempt.mjs +29 -4
- package/dist/models/capture-attempt.mjs.map +1 -1
- package/dist/models/capture-job.d.mts +106 -0
- package/dist/models/capture-job.mjs +288 -0
- package/dist/models/capture-job.mjs.map +1 -0
- package/dist/models/capture-log.d.mts +1 -1
- package/dist/models/capture-log.mjs +4 -3
- package/dist/models/capture-log.mjs.map +1 -1
- package/dist/models/capture-shard.d.mts +62 -0
- package/dist/models/capture-shard.mjs +190 -0
- package/dist/models/capture-shard.mjs.map +1 -0
- package/dist/models/changeset.d.mts +48 -0
- package/dist/models/changeset.mjs +81 -0
- package/dist/models/changeset.mjs.map +1 -0
- package/dist/models/columns.mjs +19 -0
- package/dist/models/columns.mjs.map +1 -0
- package/dist/models/comment.d.mts +4 -1
- package/dist/models/comment.mjs +7 -4
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/component.d.mts +43 -0
- package/dist/models/component.mjs +60 -0
- package/dist/models/component.mjs.map +1 -0
- package/dist/models/connections.d.mts +41 -0
- package/dist/models/connections.mjs +112 -0
- package/dist/models/connections.mjs.map +1 -0
- package/dist/models/content-ref.d.mts +1 -1
- package/dist/models/content-ref.mjs +1 -1
- package/dist/models/content-ref.mjs.map +1 -1
- package/dist/models/credentials.mjs +12 -2
- package/dist/models/credentials.mjs.map +1 -1
- package/dist/models/design-credentials.d.mts +41 -0
- package/dist/models/design-credentials.mjs +86 -0
- package/dist/models/design-credentials.mjs.map +1 -0
- package/dist/models/design-health.d.mts +16 -0
- package/dist/models/design-health.mjs +25 -0
- package/dist/models/design-health.mjs.map +1 -0
- package/dist/models/design-nodes.d.mts +83 -0
- package/dist/models/design-nodes.mjs +222 -0
- package/dist/models/design-nodes.mjs.map +1 -0
- package/dist/models/design-renders.mjs +74 -0
- package/dist/models/design-renders.mjs.map +1 -0
- package/dist/models/design-resolve.d.mts +29 -0
- package/dist/models/design-resolve.mjs +41 -0
- package/dist/models/design-resolve.mjs.map +1 -0
- package/dist/models/design.d.mts +39 -0
- package/dist/models/design.mjs +90 -0
- package/dist/models/design.mjs.map +1 -0
- package/dist/models/inherited-ratio.d.mts +21 -0
- package/dist/models/inherited-ratio.mjs +22 -0
- package/dist/models/inherited-ratio.mjs.map +1 -0
- package/dist/models/insight.d.mts +70 -0
- package/dist/models/insight.mjs +162 -0
- package/dist/models/insight.mjs.map +1 -0
- package/dist/models/label.d.mts +4 -2
- package/dist/models/label.mjs +25 -10
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.d.mts +1 -1
- package/dist/models/member.mjs +5 -4
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/notification-channel.d.mts +37 -3
- package/dist/models/notification-channel.mjs +88 -9
- package/dist/models/notification-channel.mjs.map +1 -1
- package/dist/models/notification-subscription.d.mts +1 -2
- package/dist/models/notification-subscription.mjs +7 -9
- package/dist/models/notification-subscription.mjs.map +1 -1
- package/dist/models/project-group-mapping.d.mts +6 -1
- package/dist/models/project-group-mapping.mjs +14 -4
- package/dist/models/project-group-mapping.mjs.map +1 -1
- package/dist/models/project.d.mts +9 -1
- package/dist/models/project.mjs +9 -4
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/public-origin.d.mts +63 -0
- package/dist/models/public-origin.mjs +114 -0
- package/dist/models/public-origin.mjs.map +1 -0
- package/dist/models/review-switch-off.d.mts +14 -0
- package/dist/models/review-switch-off.mjs +26 -0
- package/dist/models/review-switch-off.mjs.map +1 -0
- package/dist/models/site-settings.d.mts +23 -0
- package/dist/models/site-settings.mjs +48 -0
- package/dist/models/site-settings.mjs.map +1 -0
- package/dist/models/snapshot.d.mts +36 -1
- package/dist/models/snapshot.mjs +50 -3
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/token.d.mts +13 -1
- package/dist/models/token.mjs +23 -5
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/usage.d.mts +76 -0
- package/dist/models/usage.mjs +149 -0
- package/dist/models/usage.mjs.map +1 -0
- package/dist/models/user.d.mts +1 -1
- package/dist/models/user.mjs +4 -3
- package/dist/models/user.mjs.map +1 -1
- package/dist/models/webhook.d.mts +15 -1
- package/dist/models/webhook.mjs +33 -4
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/models.d.mts +26 -3
- package/dist/models.mjs +27 -5
- package/dist/options.d.mts +18 -0
- package/dist/options.mjs +90 -0
- package/dist/options.mjs.map +1 -0
- package/dist/paths.d.mts +19 -4
- package/dist/paths.mjs +51 -10
- package/dist/paths.mjs.map +1 -1
- package/dist/retention/ai.d.mts +18 -0
- package/dist/retention/ai.mjs +23 -0
- package/dist/retention/ai.mjs.map +1 -0
- package/dist/retention/pending.mjs +47 -0
- package/dist/retention/pending.mjs.map +1 -0
- package/dist/retention/screenshot-sweep.d.mts +51 -0
- package/dist/retention/screenshot-sweep.mjs +128 -0
- package/dist/retention/screenshot-sweep.mjs.map +1 -0
- package/dist/retention.d.mts +16 -2
- package/dist/retention.mjs +71 -14
- package/dist/retention.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +6 -0
- package/dist/schema/build-component.d.mts +25 -0
- package/dist/schema/build-review.d.mts +17 -0
- package/dist/schema/build.d.mts +7 -0
- package/dist/schema/capture-job.d.mts +60 -0
- package/dist/schema/capture-shard.d.mts +35 -0
- package/dist/schema/comment.d.mts +2 -0
- package/dist/schema/connection.d.mts +24 -0
- package/dist/schema/design.d.mts +58 -0
- package/dist/schema/insight.d.mts +54 -0
- package/dist/schema/notification.d.mts +4 -0
- package/dist/schema/project.d.mts +21 -0
- package/dist/schema/site-setting.d.mts +21 -0
- package/dist/schema/snapshot.d.mts +12 -0
- package/dist/schema/usage.d.mts +30 -0
- package/dist/schema.d.mts +11 -3
- package/dist/serverless.d.mts +21 -0
- package/dist/serverless.mjs +37 -0
- package/dist/serverless.mjs.map +1 -0
- package/dist/test-helpers/create-project.d.mts +1 -1
- package/dist/test-helpers/deferred.d.mts +39 -0
- package/dist/test-helpers/deferred.mjs +61 -0
- package/dist/test-helpers/deferred.mjs.map +1 -0
- package/dist/test-helpers/design-contract.d.mts +21 -0
- package/dist/test-helpers/design-contract.mjs +40 -0
- package/dist/test-helpers/design-contract.mjs.map +1 -0
- package/dist/test-helpers/fake-database.mjs +14 -2
- package/dist/test-helpers/fake-database.mjs.map +1 -1
- package/dist/test-helpers/fake-design.d.mts +43 -0
- package/dist/test-helpers/fake-design.mjs +114 -0
- package/dist/test-helpers/fake-design.mjs.map +1 -0
- package/dist/test-helpers/fake-notifier.d.mts +21 -0
- package/dist/test-helpers/fake-notifier.mjs +44 -0
- package/dist/test-helpers/fake-notifier.mjs.map +1 -0
- package/dist/test-helpers/fake-tables.mjs +255 -67
- package/dist/test-helpers/fake-tables.mjs.map +1 -1
- package/dist/test-helpers/manual-clock.d.mts +27 -0
- package/dist/test-helpers/manual-clock.mjs +56 -0
- package/dist/test-helpers/manual-clock.mjs.map +1 -0
- package/dist/test-helpers/notifier-contract.d.mts +7 -1
- package/dist/test-helpers/notifier-contract.mjs +32 -3
- package/dist/test-helpers/notifier-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-contract.d.mts +16 -2
- package/dist/test-helpers/queue-contract.mjs +195 -1
- package/dist/test-helpers/queue-contract.mjs.map +1 -1
- package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
- package/dist/test-helpers/queue-multi-instance.mjs +206 -0
- package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
- package/dist/test-helpers/runner-contract.mjs +11 -0
- package/dist/test-helpers/runner-contract.mjs.map +1 -1
- package/dist/test-helpers/sql-chunks.mjs +23 -0
- package/dist/test-helpers/sql-chunks.mjs.map +1 -1
- package/dist/test-helpers/storage-contract.mjs +1 -1
- package/dist/test-helpers.d.mts +9 -4
- package/dist/test-helpers.mjs +8 -3
- package/dist/types.d.mts +14 -3
- package/dist/types.mjs +21 -4
- package/dist/types.mjs.map +1 -1
- package/dist/urls.d.mts +4 -0
- package/dist/urls.mjs +11 -2
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/adaptive-concurrency.d.mts +66 -0
- package/dist/utils/adaptive-concurrency.mjs +149 -0
- package/dist/utils/adaptive-concurrency.mjs.map +1 -0
- package/dist/utils/connect-retry.d.mts +26 -0
- package/dist/utils/connect-retry.mjs +43 -0
- package/dist/utils/connect-retry.mjs.map +1 -0
- package/dist/utils/environment.d.mts +19 -0
- package/dist/utils/environment.mjs +59 -0
- package/dist/utils/environment.mjs.map +1 -0
- package/dist/utils/http.d.mts +17 -1
- package/dist/utils/http.mjs +22 -3
- package/dist/utils/http.mjs.map +1 -1
- package/dist/utils/map-adaptive.d.mts +15 -0
- package/dist/utils/map-adaptive.mjs +72 -0
- package/dist/utils/map-adaptive.mjs.map +1 -0
- package/dist/utils/map-limit.d.mts +10 -0
- package/dist/utils/map-limit.mjs +32 -0
- package/dist/utils/map-limit.mjs.map +1 -0
- package/dist/utils/page-limiter.d.mts +37 -0
- package/dist/utils/page-limiter.mjs +56 -0
- package/dist/utils/page-limiter.mjs.map +1 -0
- package/dist/utils/public-origin.d.mts +20 -0
- package/dist/utils/public-origin.mjs +75 -0
- package/dist/utils/public-origin.mjs.map +1 -0
- package/dist/utils/redact.mjs +3 -0
- package/dist/utils/redact.mjs.map +1 -1
- package/dist/utils/remote-environment.d.mts +39 -0
- package/dist/utils/remote-environment.mjs +114 -0
- package/dist/utils/remote-environment.mjs.map +1 -0
- package/dist/utils/resources.d.mts +64 -0
- package/dist/utils/resources.mjs +122 -0
- package/dist/utils/resources.mjs.map +1 -0
- package/dist/utils/screenshot-store.d.mts +15 -0
- package/dist/utils/screenshot-store.mjs +41 -0
- package/dist/utils/screenshot-store.mjs.map +1 -0
- package/dist/utils/session-gate.d.mts +22 -0
- package/dist/utils/session-gate.mjs +53 -0
- package/dist/utils/session-gate.mjs.map +1 -0
- package/dist/utils/storybook-token.d.mts +81 -0
- package/dist/utils/storybook-token.mjs +134 -0
- package/dist/utils/storybook-token.mjs.map +1 -0
- package/dist/utils/storybook-url.d.mts +15 -0
- package/dist/utils/storybook-url.mjs +21 -0
- package/dist/utils/storybook-url.mjs.map +1 -0
- package/dist/utils/timing.mjs +1 -1
- package/dist/utils/timing.mjs.map +1 -1
- package/dist/utils/zip-read.mjs +129 -0
- package/dist/utils/zip-read.mjs.map +1 -0
- package/dist/utils/zip-write.d.mts +18 -0
- package/dist/utils/zip-write.mjs +188 -0
- package/dist/utils/zip-write.mjs.map +1 -0
- package/dist/utils.d.mts +17 -3
- package/dist/utils.mjs +18 -4
- package/package.json +32 -20
- package/dist/adapter/capture-runner.mjs.map +0 -1
- package/dist/adapter/git-host/comments.d.mts +0 -41
- package/dist/adapter/git-host/comments.mjs +0 -54
- package/dist/adapter/git-host/comments.mjs.map +0 -1
- package/dist/adapter/git-host/helpers.d.mts +0 -13
- package/dist/adapter/git-host/helpers.mjs +0 -30
- package/dist/adapter/git-host/helpers.mjs.map +0 -1
- package/dist/adapter/git-host.d.mts +0 -42
- package/dist/capture/queue.d.mts +0 -38
- package/dist/capture/queue.mjs +0 -98
- package/dist/capture/queue.mjs.map +0 -1
- package/dist/capture/recovery.d.mts +0 -21
- package/dist/capture/recovery.mjs +0 -83
- package/dist/capture/recovery.mjs.map +0 -1
- package/dist/capture/status-fanout.mjs +0 -72
- package/dist/capture/status-fanout.mjs.map +0 -1
- package/dist/diff/engine.d.mts +0 -16
- package/dist/diff/engine.mjs +0 -87
- package/dist/diff/engine.mjs.map +0 -1
- package/dist/diff/options.d.mts +0 -42
- package/dist/diff/options.mjs +0 -15
- package/dist/diff/options.mjs.map +0 -1
- package/dist/models/status-config.d.mts +0 -38
- package/dist/models/status-config.mjs +0 -63
- package/dist/models/status-config.mjs.map +0 -1
- package/dist/schema/status-config.d.mts +0 -16
- package/dist/test-helpers/git-contract.d.mts +0 -11
- package/dist/test-helpers/git-contract.mjs +0 -34
- package/dist/test-helpers/git-contract.mjs.map +0 -1
- /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.mjs","names":[],"sources":["../../src/models/credentials.ts"],"sourcesContent":["/**\n * Stored-credential inventory and re-encryption for secret rotation.\n *\n * Webhook secrets,
|
|
1
|
+
{"version":3,"file":"credentials.mjs","names":[],"sources":["../../src/models/credentials.ts"],"sourcesContent":["/**\n * Stored-credential inventory and re-encryption for secret rotation.\n *\n * Webhook secrets, notification-channel secrets (chat webhooks and git tokens), and design-provider credentials are\n * encrypted under `ShelfConfig.secret`. After a rotation the old secret stays\n * available as `previousSecret` (decrypt-only); {@link probeCredentials} reports\n * which key each row needs and {@link reencryptCredentials} moves rows to the\n * current key. Neither returns or logs credential values.\n */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { decryptWithKey, encrypt, type SecretKeys } from \"../utils/encrypt.ts\";\n\n/** Tables that hold encrypted credentials, with the column to read. */\nconst SOURCES = [\n { name: \"webhooks\", table: \"webhooks\", column: \"secretEncrypted\" },\n { name: \"design credentials\", table: \"designCredentials\", column: \"tokenEncrypted\" },\n { name: \"user connections\", table: \"userConnections\", column: \"tokenEncrypted\" },\n {\n name: \"user connection refresh tokens\",\n table: \"userConnections\",\n column: \"refreshTokenEncrypted\",\n },\n { name: \"notification channels\", table: \"notificationChannels\", column: \"secretEncrypted\" },\n] as const;\n\n/** A row no configured key can decrypt. */\nexport interface UnreadableCredential {\n source: string;\n id: string;\n}\n\n/** Which key each stored credential needs. */\nexport interface CredentialProbe {\n total: number;\n /** Rows readable with the current secret. */\n current: number;\n /** Rows only readable with `previousSecret`; run re-encryption. */\n previous: number;\n unreadable: UnreadableCredential[];\n}\n\n/** Outcome of {@link reencryptCredentials}. */\nexport interface ReencryptResult {\n reencrypted: number;\n failed: number;\n unreadable: UnreadableCredential[];\n}\n\ninterface StoredCredential {\n source: (typeof SOURCES)[number];\n id: string;\n ciphertext: string;\n}\n\n/** Every non-empty encrypted value across the credential tables. */\nasync function storedCredentials(db: DatabaseAdapter): Promise<StoredCredential[]> {\n const lists = await Promise.all(\n SOURCES.map(async (source) => {\n const rows = (await db.list(db.tables[source.table])) as unknown as (Record<\n string,\n unknown\n > & {\n id: string;\n })[];\n return rows.flatMap((row) => {\n const ciphertext = row[source.column];\n return typeof ciphertext === \"string\" && ciphertext !== \"\"\n ? [{ source, id: row.id, ciphertext }]\n : [];\n });\n }),\n );\n return lists.flat();\n}\n\nfunction classify(keys: SecretKeys, ciphertext: string): \"current\" | \"previous\" | \"unreadable\" {\n try {\n return decryptWithKey(keys, ciphertext).keyUsed;\n } catch {\n return \"unreadable\";\n }\n}\n\n/** Count stored credentials by the key they need; read-only. */\nexport async function probeCredentials(\n db: DatabaseAdapter,\n keys: SecretKeys,\n): Promise<CredentialProbe> {\n const stored = await storedCredentials(db);\n const probe: CredentialProbe = { total: stored.length, current: 0, previous: 0, unreadable: [] };\n for (const item of stored) {\n const state = classify(keys, item.ciphertext);\n if (state === \"unreadable\") {\n probe.unreadable.push({ source: item.source.name, id: item.id });\n } else {\n probe[state] += 1;\n }\n }\n return probe;\n}\n\n/**\n * Re-encrypt every credential still under `keys.previous` with `keys.current`.\n * Idempotent; unreadable rows are left untouched and reported, and one failed\n * write does not stop the rest.\n */\nexport async function reencryptCredentials(\n db: DatabaseAdapter,\n keys: SecretKeys,\n): Promise<ReencryptResult> {\n const result: ReencryptResult = { reencrypted: 0, failed: 0, unreadable: [] };\n const outcomes = await Promise.all(\n (await storedCredentials(db)).map(async (item) => await reencryptOne(db, keys, item)),\n );\n for (const outcome of outcomes) {\n if (outcome.state === \"unreadable\") {\n result.unreadable.push(outcome.row);\n } else if (outcome.state !== \"skipped\") {\n result[outcome.state] += 1;\n }\n }\n return result;\n}\n\ntype Outcome =\n | { state: \"reencrypted\" | \"failed\" | \"skipped\" }\n | { state: \"unreadable\"; row: UnreadableCredential };\n\nasync function reencryptOne(\n db: DatabaseAdapter,\n keys: SecretKeys,\n item: StoredCredential,\n): Promise<Outcome> {\n let plain: { plaintext: string; keyUsed: string };\n try {\n plain = decryptWithKey(keys, item.ciphertext);\n } catch {\n return { state: \"unreadable\", row: { source: item.source.name, id: item.id } };\n }\n if (plain.keyUsed === \"current\") {\n return { state: \"skipped\" };\n }\n try {\n await db.update(db.tables[item.source.table], item.id, {\n [item.source.column]: encrypt(keys.current, plain.plaintext),\n });\n return { state: \"reencrypted\" };\n } catch {\n return { state: \"failed\" };\n }\n}\n"],"mappings":";;;;;;AAaA,MAAM,UAAU;CACd;EAAE,MAAM;EAAY,OAAO;EAAY,QAAQ;CAAkB;CACjE;EAAE,MAAM;EAAsB,OAAO;EAAqB,QAAQ;CAAiB;CACnF;EAAE,MAAM;EAAoB,OAAO;EAAmB,QAAQ;CAAiB;CAC/E;EACE,MAAM;EACN,OAAO;EACP,QAAQ;CACV;CACA;EAAE,MAAM;EAAyB,OAAO;EAAwB,QAAQ;CAAkB;AAC5F;;AAgCA,eAAe,kBAAkB,IAAkD;CAiBjF,QAAO,MAhBa,QAAQ,IAC1B,QAAQ,IAAI,OAAO,WAAW;EAO5B,QAAO,MANa,GAAG,KAAK,GAAG,OAAO,OAAO,MAAM,EAAA,CAMvC,SAAS,QAAQ;GAC3B,MAAM,aAAa,IAAI,OAAO;GAC9B,OAAO,OAAO,eAAe,YAAY,eAAe,KACpD,CAAC;IAAE;IAAQ,IAAI,IAAI;IAAI;GAAW,CAAC,IACnC,CAAC;EACP,CAAC;CACH,CAAC,CACH,EAAA,CACa,KAAK;AACpB;AAEA,SAAS,SAAS,MAAkB,YAA2D;CAC7F,IAAI;EACF,OAAO,eAAe,MAAM,UAAU,CAAC,CAAC;CAC1C,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,iBACpB,IACA,MAC0B;CAC1B,MAAM,SAAS,MAAM,kBAAkB,EAAE;CACzC,MAAM,QAAyB;EAAE,OAAO,OAAO;EAAQ,SAAS;EAAG,UAAU;EAAG,YAAY,CAAC;CAAE;CAC/F,KAAK,MAAM,QAAQ,QAAQ;EACzB,MAAM,QAAQ,SAAS,MAAM,KAAK,UAAU;EAC5C,IAAI,UAAU,cACZ,MAAM,WAAW,KAAK;GAAE,QAAQ,KAAK,OAAO;GAAM,IAAI,KAAK;EAAG,CAAC;OAE/D,MAAM,UAAU;CAEpB;CACA,OAAO;AACT;;;;;;AAOA,eAAsB,qBACpB,IACA,MAC0B;CAC1B,MAAM,SAA0B;EAAE,aAAa;EAAG,QAAQ;EAAG,YAAY,CAAC;CAAE;CAC5E,MAAM,WAAW,MAAM,QAAQ,KAC5B,MAAM,kBAAkB,EAAE,EAAA,CAAG,IAAI,OAAO,SAAS,MAAM,aAAa,IAAI,MAAM,IAAI,CAAC,CACtF;CACA,KAAK,MAAM,WAAW,UACpB,IAAI,QAAQ,UAAU,cACpB,OAAO,WAAW,KAAK,QAAQ,GAAG;MAC7B,IAAI,QAAQ,UAAU,WAC3B,OAAO,QAAQ,UAAU;CAG7B,OAAO;AACT;AAMA,eAAe,aACb,IACA,MACA,MACkB;CAClB,IAAI;CACJ,IAAI;EACF,QAAQ,eAAe,MAAM,KAAK,UAAU;CAC9C,QAAQ;EACN,OAAO;GAAE,OAAO;GAAc,KAAK;IAAE,QAAQ,KAAK,OAAO;IAAM,IAAI,KAAK;GAAG;EAAE;CAC/E;CACA,IAAI,MAAM,YAAY,WACpB,OAAO,EAAE,OAAO,UAAU;CAE5B,IAAI;EACF,MAAM,GAAG,OAAO,GAAG,OAAO,KAAK,OAAO,QAAQ,KAAK,IAAI,GACpD,KAAK,OAAO,SAAS,QAAQ,KAAK,SAAS,MAAM,SAAS,EAC7D,CAAC;EACD,OAAO,EAAE,OAAO,cAAc;CAChC,QAAQ;EACN,OAAO,EAAE,OAAO,SAAS;CAC3B;AACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { SecretInput } from "../utils/encrypt.mjs";
|
|
5
|
+
import { DesignCredential } from "../schema/design.mjs";
|
|
6
|
+
import { DesignCredentialStatus } from "./design-resolve.mjs";
|
|
7
|
+
import { Table } from "drizzle-orm";
|
|
8
|
+
//#region src/models/design-credentials.d.ts
|
|
9
|
+
/** Tables required by {@link DesignCredentialModel}. */
|
|
10
|
+
interface DesignCredentialTables {
|
|
11
|
+
designCredentials: Table;
|
|
12
|
+
}
|
|
13
|
+
/** Cache scope of the shared project credential (`design_nodes.viewer_id`); a user id marks a member's own connection. */
|
|
14
|
+
declare const PROJECT_SCOPE = "";
|
|
15
|
+
/**
|
|
16
|
+
* Data operations for design credentials. Every method addresses one
|
|
17
|
+
* `(project, provider)` row. Plaintext only ever leaves through {@link get}.
|
|
18
|
+
*/
|
|
19
|
+
declare class DesignCredentialModel {
|
|
20
|
+
private readonly tables;
|
|
21
|
+
private readonly db;
|
|
22
|
+
private readonly secret?;
|
|
23
|
+
constructor(db: DatabaseAdapter, secret?: SecretInput, tables?: DesignCredentialTables);
|
|
24
|
+
/** The credential row (never the plaintext), or null. */
|
|
25
|
+
info(projectId: string, provider: string): Promise<DesignCredential | null>;
|
|
26
|
+
/** Decrypted access token for API calls, or null when none is set. */
|
|
27
|
+
get(projectId: string, provider: string): Promise<string | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Store (or rotate) a pasted token. A new credential restarts the rotation
|
|
30
|
+
* clock (`createdAt`) and resets health to `unknown`.
|
|
31
|
+
*/
|
|
32
|
+
set(projectId: string, provider: string, token: string): Promise<void>;
|
|
33
|
+
/** Delete a credential. */
|
|
34
|
+
remove(projectId: string, provider: string): Promise<void>;
|
|
35
|
+
/** Record the last observed API outcome; never moves the rotation age. */
|
|
36
|
+
recordStatus(projectId: string, provider: string, status: DesignCredentialStatus): Promise<void>;
|
|
37
|
+
private replace;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { DesignCredentialModel, DesignCredentialTables, PROJECT_SCOPE };
|
|
41
|
+
//# sourceMappingURL=design-credentials.d.mts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { decryptCredential, encrypt } from "../utils/encrypt.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { col } from "./columns.mjs";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
8
|
+
//#region src/models/design-credentials.ts
|
|
9
|
+
/** Encrypted design-provider credentials: the project-wide token an admin pastes. */
|
|
10
|
+
/** Cache scope of the shared project credential (`design_nodes.viewer_id`); a user id marks a member's own connection. */
|
|
11
|
+
const PROJECT_SCOPE = "";
|
|
12
|
+
/**
|
|
13
|
+
* Data operations for design credentials. Every method addresses one
|
|
14
|
+
* `(project, provider)` row. Plaintext only ever leaves through {@link get}.
|
|
15
|
+
*/
|
|
16
|
+
var DesignCredentialModel = class {
|
|
17
|
+
tables;
|
|
18
|
+
db;
|
|
19
|
+
secret;
|
|
20
|
+
constructor(db, secret, tables) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.secret = secret;
|
|
23
|
+
this.tables = tables ?? { designCredentials: db.tables.designCredentials };
|
|
24
|
+
}
|
|
25
|
+
/** The credential row (never the plaintext), or null. */
|
|
26
|
+
async info(projectId, provider) {
|
|
27
|
+
return (await this.db.list(this.tables.designCredentials, {
|
|
28
|
+
where: and(eq(col(this.tables.designCredentials, "projectId"), projectId), eq(col(this.tables.designCredentials, "provider"), provider)),
|
|
29
|
+
limit: 1
|
|
30
|
+
}))[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
/** Decrypted access token for API calls, or null when none is set. */
|
|
33
|
+
async get(projectId, provider) {
|
|
34
|
+
const row = await this.info(projectId, provider);
|
|
35
|
+
return row ? decryptCredential(this.secret, row.tokenEncrypted) : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Store (or rotate) a pasted token. A new credential restarts the rotation
|
|
39
|
+
* clock (`createdAt`) and resets health to `unknown`.
|
|
40
|
+
*/
|
|
41
|
+
async set(projectId, provider, token) {
|
|
42
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
43
|
+
const values = {
|
|
44
|
+
tokenEncrypted: encrypt(this.secret, token),
|
|
45
|
+
status: "unknown",
|
|
46
|
+
checkedAt: null,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
updatedAt: now
|
|
49
|
+
};
|
|
50
|
+
if (await this.replace(projectId, provider, values)) return;
|
|
51
|
+
try {
|
|
52
|
+
await this.db.insert(this.tables.designCredentials, {
|
|
53
|
+
id: ulid(),
|
|
54
|
+
projectId,
|
|
55
|
+
provider,
|
|
56
|
+
...values
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!await this.replace(projectId, provider, values)) throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Delete a credential. */
|
|
63
|
+
async remove(projectId, provider) {
|
|
64
|
+
const existing = await this.info(projectId, provider);
|
|
65
|
+
if (existing) await this.db.remove(this.tables.designCredentials, existing.id);
|
|
66
|
+
}
|
|
67
|
+
/** Record the last observed API outcome; never moves the rotation age. */
|
|
68
|
+
async recordStatus(projectId, provider, status) {
|
|
69
|
+
const existing = await this.info(projectId, provider);
|
|
70
|
+
if (!existing) return;
|
|
71
|
+
await this.db.update(this.tables.designCredentials, existing.id, {
|
|
72
|
+
status,
|
|
73
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async replace(projectId, provider, values) {
|
|
77
|
+
const existing = await this.info(projectId, provider);
|
|
78
|
+
if (!existing) return false;
|
|
79
|
+
await this.db.update(this.tables.designCredentials, existing.id, { ...values });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { DesignCredentialModel, PROJECT_SCOPE };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=design-credentials.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-credentials.mjs","names":[],"sources":["../../src/models/design-credentials.ts"],"sourcesContent":["/** Encrypted design-provider credentials: the project-wide token an admin pastes. */\nimport { and, eq } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { DesignCredential } from \"../schema/design.ts\";\nimport { decryptCredential, encrypt, type SecretInput } from \"../utils/encrypt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\nimport { col } from \"./columns.ts\";\nimport type { DesignCredentialStatus } from \"./design-resolve.ts\";\n\n/** Tables required by {@link DesignCredentialModel}. */\nexport interface DesignCredentialTables {\n designCredentials: Table;\n}\n\n/** Cache scope of the shared project credential (`design_nodes.viewer_id`); a user id marks a member's own connection. */\nexport const PROJECT_SCOPE = \"\";\n\ntype Fields = Pick<\n DesignCredential,\n \"tokenEncrypted\" | \"status\" | \"checkedAt\" | \"createdAt\" | \"updatedAt\"\n>;\n\n/**\n * Data operations for design credentials. Every method addresses one\n * `(project, provider)` row. Plaintext only ever leaves through {@link get}.\n */\nexport class DesignCredentialModel {\n private readonly tables: DesignCredentialTables;\n private readonly db: DatabaseAdapter;\n private readonly secret?: SecretInput;\n\n constructor(db: DatabaseAdapter, secret?: SecretInput, tables?: DesignCredentialTables) {\n this.db = db;\n this.secret = secret;\n this.tables = tables ?? { designCredentials: db.tables.designCredentials };\n }\n\n /** The credential row (never the plaintext), or null. */\n async info(projectId: string, provider: string): Promise<DesignCredential | null> {\n const rows = (await this.db.list(this.tables.designCredentials, {\n where: and(\n eq(col(this.tables.designCredentials, \"projectId\"), projectId),\n eq(col(this.tables.designCredentials, \"provider\"), provider),\n ),\n limit: 1,\n })) as unknown as DesignCredential[];\n return rows[0] ?? null;\n }\n\n /** Decrypted access token for API calls, or null when none is set. */\n async get(projectId: string, provider: string): Promise<string | null> {\n const row = await this.info(projectId, provider);\n return row ? decryptCredential(this.secret, row.tokenEncrypted) : null;\n }\n\n /**\n * Store (or rotate) a pasted token. A new credential restarts the rotation\n * clock (`createdAt`) and resets health to `unknown`.\n */\n async set(projectId: string, provider: string, token: string): Promise<void> {\n const now = new Date().toISOString();\n const values: Fields = {\n tokenEncrypted: encrypt(this.secret, token),\n status: \"unknown\",\n checkedAt: null,\n createdAt: now,\n updatedAt: now,\n };\n if (await this.replace(projectId, provider, values)) return;\n try {\n await this.db.insert(this.tables.designCredentials, {\n id: ulid(),\n projectId,\n provider,\n ...values,\n });\n } catch (error) {\n // A concurrent writer inserted first (unique project/provider): rotate theirs instead.\n if (!(await this.replace(projectId, provider, values))) throw error;\n }\n }\n\n /** Delete a credential. */\n async remove(projectId: string, provider: string): Promise<void> {\n const existing = await this.info(projectId, provider);\n if (existing) await this.db.remove(this.tables.designCredentials, existing.id);\n }\n\n /** Record the last observed API outcome; never moves the rotation age. */\n async recordStatus(\n projectId: string,\n provider: string,\n status: DesignCredentialStatus,\n ): Promise<void> {\n const existing = await this.info(projectId, provider);\n if (!existing) return;\n await this.db.update(this.tables.designCredentials, existing.id, {\n status,\n checkedAt: new Date().toISOString(),\n });\n }\n\n private async replace(projectId: string, provider: string, values: Fields): Promise<boolean> {\n const existing = await this.info(projectId, provider);\n if (!existing) return false;\n await this.db.update(this.tables.designCredentials, existing.id, {\n ...values,\n });\n return true;\n }\n}\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,gBAAgB;;;;;AAW7B,IAAa,wBAAb,MAAmC;CACjC;CACA;CACA;CAEA,YAAY,IAAqB,QAAsB,QAAiC;EACtF,KAAK,KAAK;EACV,KAAK,SAAS;EACd,KAAK,SAAS,UAAU,EAAE,mBAAmB,GAAG,OAAO,kBAAkB;CAC3E;;CAGA,MAAM,KAAK,WAAmB,UAAoD;EAQhF,QAAO,MAPa,KAAK,GAAG,KAAK,KAAK,OAAO,mBAAmB;GAC9D,OAAO,IACL,GAAG,IAAI,KAAK,OAAO,mBAAmB,WAAW,GAAG,SAAS,GAC7D,GAAG,IAAI,KAAK,OAAO,mBAAmB,UAAU,GAAG,QAAQ,CAC7D;GACA,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,IAAI,WAAmB,UAA0C;EACrE,MAAM,MAAM,MAAM,KAAK,KAAK,WAAW,QAAQ;EAC/C,OAAO,MAAM,kBAAkB,KAAK,QAAQ,IAAI,cAAc,IAAI;CACpE;;;;;CAMA,MAAM,IAAI,WAAmB,UAAkB,OAA8B;EAC3E,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,SAAiB;GACrB,gBAAgB,QAAQ,KAAK,QAAQ,KAAK;GAC1C,QAAQ;GACR,WAAW;GACX,WAAW;GACX,WAAW;EACb;EACA,IAAI,MAAM,KAAK,QAAQ,WAAW,UAAU,MAAM,GAAG;EACrD,IAAI;GACF,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,mBAAmB;IAClD,IAAI,KAAK;IACT;IACA;IACA,GAAG;GACL,CAAC;EACH,SAAS,OAAO;GAEd,IAAI,CAAE,MAAM,KAAK,QAAQ,WAAW,UAAU,MAAM,GAAI,MAAM;EAChE;CACF;;CAGA,MAAM,OAAO,WAAmB,UAAiC;EAC/D,MAAM,WAAW,MAAM,KAAK,KAAK,WAAW,QAAQ;EACpD,IAAI,UAAU,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,mBAAmB,SAAS,EAAE;CAC/E;;CAGA,MAAM,aACJ,WACA,UACA,QACe;EACf,MAAM,WAAW,MAAM,KAAK,KAAK,WAAW,QAAQ;EACpD,IAAI,CAAC,UAAU;EACf,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,mBAAmB,SAAS,IAAI;GAC/D;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;CAEA,MAAc,QAAQ,WAAmB,UAAkB,QAAkC;EAC3F,MAAM,WAAW,MAAM,KAAK,KAAK,WAAW,QAAQ;EACpD,IAAI,CAAC,UAAU,OAAO;EACtB,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,mBAAmB,SAAS,IAAI,EAC/D,GAAG,OACL,CAAC;EACD,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DesignNode } from "../schema/design.mjs";
|
|
4
|
+
//#region src/models/design-health.d.ts
|
|
5
|
+
/** Cached metadata younger than this is served without a refetch. */
|
|
6
|
+
declare const DESIGN_TTL_MS: number;
|
|
7
|
+
/** True when the design changed after the baseline it is compared with was approved. */
|
|
8
|
+
declare function isDesignStale(node: Pick<DesignNode, "state" | "lastModified"> | null | undefined, baselineVersion: string | null | undefined): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Days until a credential passes its provider's maximum lifetime (negative once
|
|
11
|
+
* past). `issuedAt` is when the credential was set or replaced — never a status write.
|
|
12
|
+
*/
|
|
13
|
+
declare function credentialDaysLeft(issuedAt: string, maxAgeDays: number, now?: Date): number;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { DESIGN_TTL_MS, credentialDaysLeft, isDesignStale };
|
|
16
|
+
//# sourceMappingURL=design-health.d.mts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/models/design-health.ts
|
|
5
|
+
/** Cached metadata younger than this is served without a refetch. */
|
|
6
|
+
const DESIGN_TTL_MS = 9e5;
|
|
7
|
+
/** True when the design changed after the baseline it is compared with was approved. */
|
|
8
|
+
function isDesignStale(node, baselineVersion) {
|
|
9
|
+
if (node?.state !== "ok" || !node.lastModified || !baselineVersion) return false;
|
|
10
|
+
const modified = Date.parse(node.lastModified);
|
|
11
|
+
const approved = Date.parse(baselineVersion);
|
|
12
|
+
return Number.isFinite(modified) && Number.isFinite(approved) && modified > approved;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Days until a credential passes its provider's maximum lifetime (negative once
|
|
16
|
+
* past). `issuedAt` is when the credential was set or replaced — never a status write.
|
|
17
|
+
*/
|
|
18
|
+
function credentialDaysLeft(issuedAt, maxAgeDays, now = /* @__PURE__ */ new Date()) {
|
|
19
|
+
const ageDays = (now.getTime() - Date.parse(issuedAt)) / 864e5;
|
|
20
|
+
return Math.ceil(maxAgeDays - ageDays);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { DESIGN_TTL_MS, credentialDaysLeft, isDesignStale };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=design-health.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-health.mjs","names":[],"sources":["../../src/models/design-health.ts"],"sourcesContent":["/** Design cache freshness, staleness and credential-age rules (provider-neutral). */\nimport type { DesignNode } from \"../schema/design.ts\";\n\n/** Cached metadata younger than this is served without a refetch. */\nexport const DESIGN_TTL_MS = 15 * 60_000;\n\n/** True when the design changed after the baseline it is compared with was approved. */\nexport function isDesignStale(\n node: Pick<DesignNode, \"state\" | \"lastModified\"> | null | undefined,\n baselineVersion: string | null | undefined,\n): boolean {\n if (node?.state !== \"ok\" || !node.lastModified || !baselineVersion) return false;\n const modified = Date.parse(node.lastModified);\n const approved = Date.parse(baselineVersion);\n return Number.isFinite(modified) && Number.isFinite(approved) && modified > approved;\n}\n\n/**\n * Days until a credential passes its provider's maximum lifetime (negative once\n * past). `issuedAt` is when the credential was set or replaced — never a status write.\n */\nexport function credentialDaysLeft(\n issuedAt: string,\n maxAgeDays: number,\n now: Date = new Date(),\n): number {\n const ageDays = (now.getTime() - Date.parse(issuedAt)) / 86_400_000;\n return Math.ceil(maxAgeDays - ageDays);\n}\n"],"mappings":";;;;;AAIA,MAAa,gBAAgB;;AAG7B,SAAgB,cACd,MACA,iBACS;CACT,IAAI,MAAM,UAAU,QAAQ,CAAC,KAAK,gBAAgB,CAAC,iBAAiB,OAAO;CAC3E,MAAM,WAAW,KAAK,MAAM,KAAK,YAAY;CAC7C,MAAM,WAAW,KAAK,MAAM,eAAe;CAC3C,OAAO,OAAO,SAAS,QAAQ,KAAK,OAAO,SAAS,QAAQ,KAAK,WAAW;AAC9E;;;;;AAMA,SAAgB,mBACd,UACA,YACA,sBAAY,IAAI,KAAK,GACb;CACR,MAAM,WAAW,IAAI,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK;CACzD,OAAO,KAAK,KAAK,aAAa,OAAO;AACvC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import __tsdown_shims_path from 'node:path';
|
|
2
|
+
import __tsdown_shims_url from 'node:url';
|
|
3
|
+
import { DatabaseAdapter } from "../adapter/database.mjs";
|
|
4
|
+
import { Logger } from "../logger.mjs";
|
|
5
|
+
import { DesignProvider } from "../adapter/design.mjs";
|
|
6
|
+
import { StorageAdapter } from "../adapter/storage.mjs";
|
|
7
|
+
import { ConnectionModel } from "./connections.mjs";
|
|
8
|
+
import { DesignNode } from "../schema/design.mjs";
|
|
9
|
+
import { DesignCredentialModel } from "./design-credentials.mjs";
|
|
10
|
+
import { Table } from "drizzle-orm";
|
|
11
|
+
import { Buffer } from "node:buffer";
|
|
12
|
+
//#region src/models/design-nodes.d.ts
|
|
13
|
+
/** Tables required by {@link DesignNodeModel}. */
|
|
14
|
+
interface DesignNodeTables {
|
|
15
|
+
designNodes: Table;
|
|
16
|
+
}
|
|
17
|
+
/** Options of {@link DesignNodeModel.refreshMany}. */
|
|
18
|
+
interface RefreshOptions {
|
|
19
|
+
now?: Date;
|
|
20
|
+
/** Cancels in-flight provider calls (e.g. a page-render budget). */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** The visiting user: their own connection is used when the project has no credential. */
|
|
23
|
+
viewerId?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Cache of design file/node metadata and node renders with TTL refresh. */
|
|
26
|
+
declare class DesignNodeModel {
|
|
27
|
+
private readonly tables;
|
|
28
|
+
private readonly renders;
|
|
29
|
+
private readonly logger;
|
|
30
|
+
private readonly connections;
|
|
31
|
+
private readonly db;
|
|
32
|
+
private readonly credentials;
|
|
33
|
+
private readonly providers;
|
|
34
|
+
constructor(db: DatabaseAdapter, credentials: DesignCredentialModel, providers: readonly DesignProvider[], options?: {
|
|
35
|
+
tables?: DesignNodeTables;
|
|
36
|
+
logger?: Logger;
|
|
37
|
+
storage?: StorageAdapter;
|
|
38
|
+
/** Members' own connections, used when the project has no credential. */
|
|
39
|
+
connections?: ConnectionModel;
|
|
40
|
+
});
|
|
41
|
+
/** Cached row by id, scoped to the project, or null. */
|
|
42
|
+
get(projectId: string, id: string): Promise<DesignNode | null>;
|
|
43
|
+
/** Cached render bytes of a node, or null when it has none (or storage is not wired). */
|
|
44
|
+
readRender(node: DesignNode): Promise<Buffer | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Cached rows for the given design URLs, keyed by URL (unparseable or uncached URLs are
|
|
47
|
+
* absent). A row fetched with someone's own connection is only visible to that viewer.
|
|
48
|
+
*/
|
|
49
|
+
cachedFor(projectId: string, urls: readonly string[], viewerId?: string): Promise<Map<string, DesignNode>>;
|
|
50
|
+
/** Refresh one link (see {@link refreshMany}); null for unsupported URLs or nothing cached. */
|
|
51
|
+
refresh(projectId: string, url: string, now?: Date, viewerId?: string): Promise<DesignNode | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Refresh the given links' metadata and renders when missing or past the TTL.
|
|
54
|
+
* Links of one file share one metadata call and one render call, and files run
|
|
55
|
+
* with bounded concurrency. Transient failures keep the previous cache; auth
|
|
56
|
+
* failures update the credential's health. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
refreshMany(projectId: string, urls: readonly string[], options?: RefreshOptions): Promise<Map<string, DesignNode | null>>;
|
|
59
|
+
/** Refresh due links grouped by provider and file, files in parallel (bounded). */
|
|
60
|
+
private refreshDue;
|
|
61
|
+
private refreshFile;
|
|
62
|
+
/** Upsert a row per item that got a usable answer (`error` keeps its cached row), keyed by URL. */
|
|
63
|
+
private storeAll;
|
|
64
|
+
/** A client for the provider of `items` bound to the items' credential scope, or null without one. */
|
|
65
|
+
private clientFor;
|
|
66
|
+
/**
|
|
67
|
+
* Whose credential serves a provider: the project's when set (it is shared and
|
|
68
|
+
* wins), else the viewer's own connection, else the project scope (nothing to fetch with).
|
|
69
|
+
*/
|
|
70
|
+
private scopeFor;
|
|
71
|
+
/** The shared row, else the viewer's own (a connected member's entries are never served to others). */
|
|
72
|
+
private visible;
|
|
73
|
+
private lookup;
|
|
74
|
+
/**
|
|
75
|
+
* Upsert the row. Non-ok states keep the last good name/version/modified time
|
|
76
|
+
* (an auth failure must not erase the label); a deleted node also drops its
|
|
77
|
+
* cached render, so a design that no longer exists is never shown as reference.
|
|
78
|
+
*/
|
|
79
|
+
private store;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { DesignNodeModel, DesignNodeTables, RefreshOptions };
|
|
83
|
+
//# sourceMappingURL=design-nodes.d.mts.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { parseDesignUrl } from "../adapter/design.mjs";
|
|
5
|
+
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { col } from "./columns.mjs";
|
|
7
|
+
import { mapLimit } from "../utils/map-limit.mjs";
|
|
8
|
+
import "./design-credentials.mjs";
|
|
9
|
+
import "./design-health.mjs";
|
|
10
|
+
import { DesignRenderStore } from "./design-renders.mjs";
|
|
11
|
+
import { and, eq } from "drizzle-orm";
|
|
12
|
+
//#region src/models/design-nodes.ts
|
|
13
|
+
/** Design metadata + render cache: TTL refresh, batched per file, bounded concurrency, abortable. */
|
|
14
|
+
/** Most files refreshed in parallel by one {@link DesignNodeModel.refreshMany} call. */
|
|
15
|
+
const FILE_CONCURRENCY = 3;
|
|
16
|
+
/** Cache of design file/node metadata and node renders with TTL refresh. */
|
|
17
|
+
var DesignNodeModel = class {
|
|
18
|
+
tables;
|
|
19
|
+
renders;
|
|
20
|
+
logger;
|
|
21
|
+
connections;
|
|
22
|
+
db;
|
|
23
|
+
credentials;
|
|
24
|
+
providers;
|
|
25
|
+
constructor(db, credentials, providers, options = {}) {
|
|
26
|
+
this.db = db;
|
|
27
|
+
this.credentials = credentials;
|
|
28
|
+
this.providers = providers;
|
|
29
|
+
this.tables = options.tables ?? { designNodes: db.tables.designNodes };
|
|
30
|
+
this.renders = new DesignRenderStore(db, this.tables.designNodes, options.storage, options.logger);
|
|
31
|
+
this.logger = options.logger;
|
|
32
|
+
this.connections = options.connections;
|
|
33
|
+
}
|
|
34
|
+
/** Cached row by id, scoped to the project, or null. */
|
|
35
|
+
async get(projectId, id) {
|
|
36
|
+
const row = await this.db.get(this.tables.designNodes, id);
|
|
37
|
+
return row?.projectId === projectId ? row : null;
|
|
38
|
+
}
|
|
39
|
+
/** Cached render bytes of a node, or null when it has none (or storage is not wired). */
|
|
40
|
+
async readRender(node) {
|
|
41
|
+
return await this.renders.read(node);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Cached rows for the given design URLs, keyed by URL (unparseable or uncached URLs are
|
|
45
|
+
* absent). A row fetched with someone's own connection is only visible to that viewer.
|
|
46
|
+
*/
|
|
47
|
+
async cachedFor(projectId, urls, viewerId) {
|
|
48
|
+
const rows = await Promise.all([...new Set(urls)].map(async (url) => {
|
|
49
|
+
const parsed = parseDesignUrl(this.providers, url);
|
|
50
|
+
return [url, parsed ? await this.visible(projectId, parsed.provider.metadata.kind, parsed.ref, viewerId) : null];
|
|
51
|
+
}));
|
|
52
|
+
return new Map(rows.flatMap(([url, row]) => row ? [[url, row]] : []));
|
|
53
|
+
}
|
|
54
|
+
/** Refresh one link (see {@link refreshMany}); null for unsupported URLs or nothing cached. */
|
|
55
|
+
async refresh(projectId, url, now, viewerId) {
|
|
56
|
+
return (await this.refreshMany(projectId, [url], {
|
|
57
|
+
...now ? { now } : {},
|
|
58
|
+
...viewerId ? { viewerId } : {}
|
|
59
|
+
})).get(url) ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Refresh the given links' metadata and renders when missing or past the TTL.
|
|
63
|
+
* Links of one file share one metadata call and one render call, and files run
|
|
64
|
+
* with bounded concurrency. Transient failures keep the previous cache; auth
|
|
65
|
+
* failures update the credential's health. Never throws.
|
|
66
|
+
*/
|
|
67
|
+
async refreshMany(projectId, urls, options = {}) {
|
|
68
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
69
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
70
|
+
const known = (await Promise.all([...new Set(urls)].map(async (url) => {
|
|
71
|
+
const parsed = parseDesignUrl(this.providers, url);
|
|
72
|
+
if (!parsed) return null;
|
|
73
|
+
const kind = parsed.provider.metadata.kind;
|
|
74
|
+
const scope = await memo(scopes, kind, async () => {
|
|
75
|
+
return await this.scopeFor(projectId, kind, options.viewerId);
|
|
76
|
+
});
|
|
77
|
+
const cached = await this.lookup(projectId, kind, parsed.ref, scope);
|
|
78
|
+
return {
|
|
79
|
+
url,
|
|
80
|
+
provider: parsed.provider,
|
|
81
|
+
ref: parsed.ref,
|
|
82
|
+
cached,
|
|
83
|
+
scope
|
|
84
|
+
};
|
|
85
|
+
}))).filter((item) => item !== null);
|
|
86
|
+
const results = new Map(known.map((item) => [item.url, item.cached]));
|
|
87
|
+
const due = known.filter((item) => isDue(item.cached, now));
|
|
88
|
+
if (due.length === 0) return results;
|
|
89
|
+
const fresh = await this.refreshDue(projectId, due, now, callOptions(this.logger, options));
|
|
90
|
+
for (const [url, node] of fresh) results.set(url, node);
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
/** Refresh due links grouped by provider and file, files in parallel (bounded). */
|
|
94
|
+
async refreshDue(projectId, due, now, call) {
|
|
95
|
+
const byFile = [...Map.groupBy(due, (item) => `${item.provider.metadata.kind}\u0000${item.ref.fileKey}`)];
|
|
96
|
+
const batches = await mapLimit(byFile, FILE_CONCURRENCY, async ([, items]) => {
|
|
97
|
+
return await this.refreshFile(projectId, items, now, call);
|
|
98
|
+
});
|
|
99
|
+
const merged = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const batch of batches) for (const [url, node] of batch) merged.set(url, node);
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
async refreshFile(projectId, items, now, call) {
|
|
104
|
+
const client = await this.clientFor(projectId, items);
|
|
105
|
+
const first = items[0];
|
|
106
|
+
if (!client || !first) return /* @__PURE__ */ new Map();
|
|
107
|
+
const { provider, ref } = first;
|
|
108
|
+
const metas = await client.fetchMetas(ref.fileKey, items.map((item) => item.ref), call);
|
|
109
|
+
const answered = [...metas.values()].find((meta) => meta.state !== "error");
|
|
110
|
+
if (answered && first.scope === "") await this.credentials.recordStatus(projectId, provider.metadata.kind, statusFor(answered.state));
|
|
111
|
+
const stored = await this.storeAll(projectId, items, metas, now);
|
|
112
|
+
return await this.renders.ensure(projectId, client, ref.fileKey, stored, call);
|
|
113
|
+
}
|
|
114
|
+
/** Upsert a row per item that got a usable answer (`error` keeps its cached row), keyed by URL. */
|
|
115
|
+
async storeAll(projectId, items, metas, now) {
|
|
116
|
+
const rows = await Promise.all(items.map(async (item) => {
|
|
117
|
+
const meta = metas.get(item.ref.nodeId ?? "");
|
|
118
|
+
const row = meta && meta.state !== "error" ? await this.store(projectId, item, meta, now) : null;
|
|
119
|
+
return row ? [item.url, row] : null;
|
|
120
|
+
}));
|
|
121
|
+
return new Map(rows.filter((entry) => entry !== null));
|
|
122
|
+
}
|
|
123
|
+
/** A client for the provider of `items` bound to the items' credential scope, or null without one. */
|
|
124
|
+
async clientFor(projectId, items) {
|
|
125
|
+
const first = items[0];
|
|
126
|
+
if (!first) return null;
|
|
127
|
+
const { provider, scope } = first;
|
|
128
|
+
const kind = provider.metadata.kind;
|
|
129
|
+
const own = scope !== "";
|
|
130
|
+
const credential = own ? await this.connections?.accessToken(scope, kind, provider.oauth).catch(() => null) : await this.credentials.get(projectId, kind).catch(() => null);
|
|
131
|
+
if (!credential) return null;
|
|
132
|
+
return provider.create({
|
|
133
|
+
credential,
|
|
134
|
+
authType: own ? "oauth" : "token",
|
|
135
|
+
...this.logger ? { logger: this.logger } : {}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whose credential serves a provider: the project's when set (it is shared and
|
|
140
|
+
* wins), else the viewer's own connection, else the project scope (nothing to fetch with).
|
|
141
|
+
*/
|
|
142
|
+
async scopeFor(projectId, provider, viewerId) {
|
|
143
|
+
if (!viewerId || !this.connections || await this.credentials.info(projectId, provider)) return "";
|
|
144
|
+
return await this.connections.info(viewerId, provider) ? viewerId : "";
|
|
145
|
+
}
|
|
146
|
+
/** The shared row, else the viewer's own (a connected member's entries are never served to others). */
|
|
147
|
+
async visible(projectId, provider, ref, viewerId) {
|
|
148
|
+
const shared = await this.lookup(projectId, provider, ref, "");
|
|
149
|
+
if (shared || !viewerId) return shared;
|
|
150
|
+
return await this.lookup(projectId, provider, ref, viewerId);
|
|
151
|
+
}
|
|
152
|
+
async lookup(projectId, provider, ref, scope) {
|
|
153
|
+
return (await this.db.list(this.tables.designNodes, {
|
|
154
|
+
where: and(eq(col(this.tables.designNodes, "projectId"), projectId), eq(col(this.tables.designNodes, "provider"), provider), eq(col(this.tables.designNodes, "viewerId"), scope), eq(col(this.tables.designNodes, "fileKey"), ref.fileKey), eq(col(this.tables.designNodes, "nodeId"), ref.nodeId ?? "")),
|
|
155
|
+
limit: 1
|
|
156
|
+
}))[0] ?? null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Upsert the row. Non-ok states keep the last good name/version/modified time
|
|
160
|
+
* (an auth failure must not erase the label); a deleted node also drops its
|
|
161
|
+
* cached render, so a design that no longer exists is never shown as reference.
|
|
162
|
+
*/
|
|
163
|
+
async store(projectId, item, meta, now) {
|
|
164
|
+
const { ref, cached } = item;
|
|
165
|
+
const stamp = now.toISOString();
|
|
166
|
+
const keep = meta.state !== "ok" && cached;
|
|
167
|
+
const values = {
|
|
168
|
+
name: keep ? cached.name : meta.name,
|
|
169
|
+
version: keep ? cached.version : meta.version,
|
|
170
|
+
lastModified: keep ? cached.lastModified : meta.lastModified,
|
|
171
|
+
state: meta.state,
|
|
172
|
+
fetchedAt: stamp,
|
|
173
|
+
updatedAt: stamp
|
|
174
|
+
};
|
|
175
|
+
if (!cached) return await this.db.insert(this.tables.designNodes, {
|
|
176
|
+
id: ulid(),
|
|
177
|
+
projectId,
|
|
178
|
+
provider: item.provider.metadata.kind,
|
|
179
|
+
viewerId: item.scope,
|
|
180
|
+
fileKey: ref.fileKey,
|
|
181
|
+
nodeId: ref.nodeId ?? "",
|
|
182
|
+
renderPath: null,
|
|
183
|
+
renderVersion: null,
|
|
184
|
+
createdAt: stamp,
|
|
185
|
+
...values
|
|
186
|
+
});
|
|
187
|
+
const dropRender = meta.state === "deleted" && cached.renderPath !== null;
|
|
188
|
+
if (dropRender) await this.renders.remove(cached);
|
|
189
|
+
return await this.db.update(this.tables.designNodes, cached.id, {
|
|
190
|
+
...values,
|
|
191
|
+
...dropRender ? {
|
|
192
|
+
renderPath: null,
|
|
193
|
+
renderVersion: null
|
|
194
|
+
} : {}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
/** Never fetched, or cached longer ago than the TTL. */
|
|
199
|
+
function isDue(cached, now) {
|
|
200
|
+
return !cached || now.getTime() - Date.parse(cached.fetchedAt) >= 9e5;
|
|
201
|
+
}
|
|
202
|
+
function callOptions(logger, options) {
|
|
203
|
+
return {
|
|
204
|
+
...logger ? { logger } : {},
|
|
205
|
+
...options.signal ? { signal: options.signal } : {}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function statusFor(state) {
|
|
209
|
+
return state === "unauthorized" || state === "forbidden" ? state : "ok";
|
|
210
|
+
}
|
|
211
|
+
/** Run `load` once per key, sharing the in-flight promise between concurrent callers. */
|
|
212
|
+
async function memo(cache, key, load) {
|
|
213
|
+
const hit = cache.get(key);
|
|
214
|
+
if (hit) return await hit;
|
|
215
|
+
const created = load();
|
|
216
|
+
cache.set(key, created);
|
|
217
|
+
return await created;
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
export { DesignNodeModel };
|
|
221
|
+
|
|
222
|
+
//# sourceMappingURL=design-nodes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-nodes.mjs","names":[],"sources":["../../src/models/design-nodes.ts"],"sourcesContent":["/** Design metadata + render cache: TTL refresh, batched per file, bounded concurrency, abortable. */\nimport { and, eq } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { Buffer } from \"node:buffer\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type {\n DesignCallOptions,\n DesignClient,\n DesignMeta,\n DesignProvider,\n DesignRef,\n} from \"../adapters/design.ts\";\nimport { parseDesignUrl } from \"../adapters/design.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport type { DesignNode } from \"../schema/design.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\nimport { col } from \"./columns.ts\";\nimport type { ConnectionModel } from \"./connections.ts\";\nimport { PROJECT_SCOPE, type DesignCredentialModel } from \"./design-credentials.ts\";\nimport { DESIGN_TTL_MS } from \"./design-health.ts\";\nimport { DesignRenderStore } from \"./design-renders.ts\";\nimport type { DesignCredentialStatus } from \"./design-resolve.ts\";\n\n/** Most files refreshed in parallel by one {@link DesignNodeModel.refreshMany} call. */\nconst FILE_CONCURRENCY = 3;\n\n/** Tables required by {@link DesignNodeModel}. */\nexport interface DesignNodeTables {\n designNodes: Table;\n}\n\n/** Options of {@link DesignNodeModel.refreshMany}. */\nexport interface RefreshOptions {\n now?: Date;\n /** Cancels in-flight provider calls (e.g. a page-render budget). */\n signal?: AbortSignal;\n /** The visiting user: their own connection is used when the project has no credential. */\n viewerId?: string;\n}\n\ninterface Due {\n url: string;\n provider: DesignProvider;\n ref: DesignRef;\n cached: DesignNode | null;\n /** Whose credential fetches this link: {@link PROJECT_SCOPE} or the viewer's user id. */\n scope: string;\n}\n\n/** Cache of design file/node metadata and node renders with TTL refresh. */\nexport class DesignNodeModel {\n private readonly tables: DesignNodeTables;\n private readonly renders: DesignRenderStore;\n private readonly logger: Logger | undefined;\n private readonly connections: ConnectionModel | undefined;\n private readonly db: DatabaseAdapter;\n private readonly credentials: DesignCredentialModel;\n private readonly providers: readonly DesignProvider[];\n\n constructor(\n db: DatabaseAdapter,\n credentials: DesignCredentialModel,\n providers: readonly DesignProvider[],\n options: {\n tables?: DesignNodeTables;\n logger?: Logger;\n storage?: StorageAdapter;\n /** Members' own connections, used when the project has no credential. */\n connections?: ConnectionModel;\n } = {},\n ) {\n this.db = db;\n this.credentials = credentials;\n this.providers = providers;\n this.tables = options.tables ?? { designNodes: db.tables.designNodes };\n this.renders = new DesignRenderStore(\n db,\n this.tables.designNodes,\n options.storage,\n options.logger,\n );\n this.logger = options.logger;\n this.connections = options.connections;\n }\n\n /** Cached row by id, scoped to the project, or null. */\n async get(projectId: string, id: string): Promise<DesignNode | null> {\n const row = (await this.db.get(this.tables.designNodes, id)) as unknown as DesignNode | null;\n return row?.projectId === projectId ? row : null;\n }\n\n /** Cached render bytes of a node, or null when it has none (or storage is not wired). */\n async readRender(node: DesignNode): Promise<Buffer | null> {\n return await this.renders.read(node);\n }\n\n /**\n * Cached rows for the given design URLs, keyed by URL (unparseable or uncached URLs are\n * absent). A row fetched with someone's own connection is only visible to that viewer.\n */\n async cachedFor(\n projectId: string,\n urls: readonly string[],\n viewerId?: string,\n ): Promise<Map<string, DesignNode>> {\n const rows = await Promise.all(\n [...new Set(urls)].map(async (url) => {\n const parsed = parseDesignUrl(this.providers, url);\n const row = parsed\n ? await this.visible(projectId, parsed.provider.metadata.kind, parsed.ref, viewerId)\n : null;\n return [url, row] as const;\n }),\n );\n return new Map(rows.flatMap(([url, row]) => (row ? [[url, row] as const] : [])));\n }\n\n /** Refresh one link (see {@link refreshMany}); null for unsupported URLs or nothing cached. */\n async refresh(\n projectId: string,\n url: string,\n now?: Date,\n viewerId?: string,\n ): Promise<DesignNode | null> {\n const results = await this.refreshMany(projectId, [url], {\n ...(now ? { now } : {}),\n ...(viewerId ? { viewerId } : {}),\n });\n return results.get(url) ?? null;\n }\n\n /**\n * Refresh the given links' metadata and renders when missing or past the TTL.\n * Links of one file share one metadata call and one render call, and files run\n * with bounded concurrency. Transient failures keep the previous cache; auth\n * failures update the credential's health. Never throws.\n */\n async refreshMany(\n projectId: string,\n urls: readonly string[],\n options: RefreshOptions = {},\n ): Promise<Map<string, DesignNode | null>> {\n const now = options.now ?? new Date();\n const scopes = new Map<string, Promise<string>>();\n const lookups = await Promise.all(\n [...new Set(urls)].map(async (url) => {\n const parsed = parseDesignUrl(this.providers, url);\n if (!parsed) return null;\n const kind = parsed.provider.metadata.kind;\n const scope = await memo(scopes, kind, async () => {\n return await this.scopeFor(projectId, kind, options.viewerId);\n });\n const cached = await this.lookup(projectId, kind, parsed.ref, scope);\n return { url, provider: parsed.provider, ref: parsed.ref, cached, scope };\n }),\n );\n const known = lookups.filter((item) => item !== null);\n const results = new Map<string, DesignNode | null>(\n known.map((item) => [item.url, item.cached]),\n );\n const due = known.filter((item) => isDue(item.cached, now));\n if (due.length === 0) return results;\n const fresh = await this.refreshDue(projectId, due, now, callOptions(this.logger, options));\n for (const [url, node] of fresh) results.set(url, node);\n return results;\n }\n\n /** Refresh due links grouped by provider and file, files in parallel (bounded). */\n private async refreshDue(\n projectId: string,\n due: Due[],\n now: Date,\n call: DesignCallOptions,\n ): Promise<Map<string, DesignNode>> {\n const byFile = [\n ...Map.groupBy(due, (item) => `${item.provider.metadata.kind}\\u0000${item.ref.fileKey}`),\n ];\n const batches = await mapLimit(byFile, FILE_CONCURRENCY, async ([, items]) => {\n return await this.refreshFile(projectId, items, now, call);\n });\n const merged = new Map<string, DesignNode>();\n for (const batch of batches) {\n for (const [url, node] of batch) merged.set(url, node);\n }\n return merged;\n }\n\n private async refreshFile(\n projectId: string,\n items: Due[],\n now: Date,\n call: DesignCallOptions,\n ): Promise<Map<string, DesignNode>> {\n const client = await this.clientFor(projectId, items);\n const first = items[0];\n if (!client || !first) return new Map();\n const { provider, ref } = first;\n const metas = await client.fetchMetas(\n ref.fileKey,\n items.map((item) => item.ref),\n call,\n );\n const answered = [...metas.values()].find((meta) => meta.state !== \"error\");\n if (answered && first.scope === PROJECT_SCOPE) {\n await this.credentials.recordStatus(\n projectId,\n provider.metadata.kind,\n statusFor(answered.state),\n );\n }\n const stored = await this.storeAll(projectId, items, metas, now);\n return await this.renders.ensure(projectId, client, ref.fileKey, stored, call);\n }\n\n /** Upsert a row per item that got a usable answer (`error` keeps its cached row), keyed by URL. */\n private async storeAll(\n projectId: string,\n items: Due[],\n metas: Map<string, DesignMeta>,\n now: Date,\n ): Promise<Map<string, DesignNode>> {\n const rows = await Promise.all(\n items.map(async (item) => {\n const meta = metas.get(item.ref.nodeId ?? \"\");\n const row =\n meta && meta.state !== \"error\" ? await this.store(projectId, item, meta, now) : null;\n return row ? ([item.url, row] as const) : null;\n }),\n );\n return new Map(rows.filter((entry) => entry !== null));\n }\n\n /** A client for the provider of `items` bound to the items' credential scope, or null without one. */\n private async clientFor(projectId: string, items: Due[]): Promise<DesignClient | null> {\n const first = items[0];\n if (!first) return null;\n const { provider, scope } = first;\n const kind = provider.metadata.kind;\n const own = scope !== PROJECT_SCOPE;\n const credential = own\n ? await this.connections?.accessToken(scope, kind, provider.oauth).catch(() => null)\n : await this.credentials.get(projectId, kind).catch(() => null);\n if (!credential) return null;\n return provider.create({\n credential,\n authType: own ? \"oauth\" : \"token\",\n ...(this.logger ? { logger: this.logger } : {}),\n });\n }\n\n /**\n * Whose credential serves a provider: the project's when set (it is shared and\n * wins), else the viewer's own connection, else the project scope (nothing to fetch with).\n */\n private async scopeFor(projectId: string, provider: string, viewerId?: string): Promise<string> {\n if (!viewerId || !this.connections || (await this.credentials.info(projectId, provider))) {\n return PROJECT_SCOPE;\n }\n return (await this.connections.info(viewerId, provider)) ? viewerId : PROJECT_SCOPE;\n }\n\n /** The shared row, else the viewer's own (a connected member's entries are never served to others). */\n private async visible(\n projectId: string,\n provider: string,\n ref: DesignRef,\n viewerId?: string,\n ): Promise<DesignNode | null> {\n const shared = await this.lookup(projectId, provider, ref, PROJECT_SCOPE);\n if (shared || !viewerId) return shared;\n return await this.lookup(projectId, provider, ref, viewerId);\n }\n\n private async lookup(\n projectId: string,\n provider: string,\n ref: DesignRef,\n scope: string,\n ): Promise<DesignNode | null> {\n const rows = (await this.db.list(this.tables.designNodes, {\n where: and(\n eq(col(this.tables.designNodes, \"projectId\"), projectId),\n eq(col(this.tables.designNodes, \"provider\"), provider),\n eq(col(this.tables.designNodes, \"viewerId\"), scope),\n eq(col(this.tables.designNodes, \"fileKey\"), ref.fileKey),\n eq(col(this.tables.designNodes, \"nodeId\"), ref.nodeId ?? \"\"),\n ),\n limit: 1,\n })) as unknown as DesignNode[];\n return rows[0] ?? null;\n }\n\n /**\n * Upsert the row. Non-ok states keep the last good name/version/modified time\n * (an auth failure must not erase the label); a deleted node also drops its\n * cached render, so a design that no longer exists is never shown as reference.\n */\n private async store(\n projectId: string,\n item: Due,\n meta: DesignMeta,\n now: Date,\n ): Promise<DesignNode> {\n const { ref, cached } = item;\n const stamp = now.toISOString();\n const keep = meta.state !== \"ok\" && cached;\n const values = {\n name: keep ? cached.name : meta.name,\n version: keep ? cached.version : meta.version,\n lastModified: keep ? cached.lastModified : meta.lastModified,\n state: meta.state,\n fetchedAt: stamp,\n updatedAt: stamp,\n };\n if (!cached) {\n return (await this.db.insert(this.tables.designNodes, {\n id: ulid(),\n projectId,\n provider: item.provider.metadata.kind,\n viewerId: item.scope,\n fileKey: ref.fileKey,\n nodeId: ref.nodeId ?? \"\",\n renderPath: null,\n renderVersion: null,\n createdAt: stamp,\n ...values,\n })) as unknown as DesignNode;\n }\n const dropRender = meta.state === \"deleted\" && cached.renderPath !== null;\n if (dropRender) await this.renders.remove(cached);\n return (await this.db.update(this.tables.designNodes, cached.id, {\n ...values,\n ...(dropRender ? { renderPath: null, renderVersion: null } : {}),\n })) as unknown as DesignNode;\n }\n}\n\n/** Never fetched, or cached longer ago than the TTL. */\nfunction isDue(cached: DesignNode | null, now: Date): boolean {\n return !cached || now.getTime() - Date.parse(cached.fetchedAt) >= DESIGN_TTL_MS;\n}\n\nfunction callOptions(logger: Logger | undefined, options: RefreshOptions): DesignCallOptions {\n return {\n ...(logger ? { logger } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n };\n}\n\nfunction statusFor(state: DesignMeta[\"state\"]): DesignCredentialStatus {\n return state === \"unauthorized\" || state === \"forbidden\" ? state : \"ok\";\n}\n\n/** Run `load` once per key, sharing the in-flight promise between concurrent callers. */\nasync function memo<T>(\n cache: Map<string, Promise<T>>,\n key: string,\n load: () => Promise<T>,\n): Promise<T> {\n const hit = cache.get(key);\n if (hit) return await hit;\n const created = load();\n cache.set(key, created);\n return await created;\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,MAAM,mBAAmB;;AA0BzB,IAAa,kBAAb,MAA6B;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,IACA,aACA,WACA,UAMI,CAAC,GACL;EACA,KAAK,KAAK;EACV,KAAK,cAAc;EACnB,KAAK,YAAY;EACjB,KAAK,SAAS,QAAQ,UAAU,EAAE,aAAa,GAAG,OAAO,YAAY;EACrE,KAAK,UAAU,IAAI,kBACjB,IACA,KAAK,OAAO,aACZ,QAAQ,SACR,QAAQ,MACV;EACA,KAAK,SAAS,QAAQ;EACtB,KAAK,cAAc,QAAQ;CAC7B;;CAGA,MAAM,IAAI,WAAmB,IAAwC;EACnE,MAAM,MAAO,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,aAAa,EAAE;EAC1D,OAAO,KAAK,cAAc,YAAY,MAAM;CAC9C;;CAGA,MAAM,WAAW,MAA0C;EACzD,OAAO,MAAM,KAAK,QAAQ,KAAK,IAAI;CACrC;;;;;CAMA,MAAM,UACJ,WACA,MACA,UACkC;EAClC,MAAM,OAAO,MAAM,QAAQ,IACzB,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,QAAQ;GACpC,MAAM,SAAS,eAAe,KAAK,WAAW,GAAG;GAIjD,OAAO,CAAC,KAHI,SACR,MAAM,KAAK,QAAQ,WAAW,OAAO,SAAS,SAAS,MAAM,OAAO,KAAK,QAAQ,IACjF,IACY;EAClB,CAAC,CACH;EACA,OAAO,IAAI,IAAI,KAAK,SAAS,CAAC,KAAK,SAAU,MAAM,CAAC,CAAC,KAAK,GAAG,CAAU,IAAI,CAAC,CAAE,CAAC;CACjF;;CAGA,MAAM,QACJ,WACA,KACA,KACA,UAC4B;EAK5B,QAAO,MAJe,KAAK,YAAY,WAAW,CAAC,GAAG,GAAG;GACvD,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;GACrB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACjC,CAAC,EAAA,CACc,IAAI,GAAG,KAAK;CAC7B;;;;;;;CAQA,MAAM,YACJ,WACA,MACA,UAA0B,CAAC,GACc;EACzC,MAAM,MAAM,QAAQ,uBAAO,IAAI,KAAK;EACpC,MAAM,yBAAS,IAAI,IAA6B;EAahD,MAAM,SAAQ,MAZQ,QAAQ,IAC5B,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,QAAQ;GACpC,MAAM,SAAS,eAAe,KAAK,WAAW,GAAG;GACjD,IAAI,CAAC,QAAQ,OAAO;GACpB,MAAM,OAAO,OAAO,SAAS,SAAS;GACtC,MAAM,QAAQ,MAAM,KAAK,QAAQ,MAAM,YAAY;IACjD,OAAO,MAAM,KAAK,SAAS,WAAW,MAAM,QAAQ,QAAQ;GAC9D,CAAC;GACD,MAAM,SAAS,MAAM,KAAK,OAAO,WAAW,MAAM,OAAO,KAAK,KAAK;GACnE,OAAO;IAAE;IAAK,UAAU,OAAO;IAAU,KAAK,OAAO;IAAK;IAAQ;GAAM;EAC1E,CAAC,CACH,EAAA,CACsB,QAAQ,SAAS,SAAS,IAAI;EACpD,MAAM,UAAU,IAAI,IAClB,MAAM,KAAK,SAAS,CAAC,KAAK,KAAK,KAAK,MAAM,CAAC,CAC7C;EACA,MAAM,MAAM,MAAM,QAAQ,SAAS,MAAM,KAAK,QAAQ,GAAG,CAAC;EAC1D,IAAI,IAAI,WAAW,GAAG,OAAO;EAC7B,MAAM,QAAQ,MAAM,KAAK,WAAW,WAAW,KAAK,KAAK,YAAY,KAAK,QAAQ,OAAO,CAAC;EAC1F,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI;EACtD,OAAO;CACT;;CAGA,MAAc,WACZ,WACA,KACA,KACA,MACkC;EAClC,MAAM,SAAS,CACb,GAAG,IAAI,QAAQ,MAAM,SAAS,GAAG,KAAK,SAAS,SAAS,KAAK,QAAQ,KAAK,IAAI,SAAS,CACzF;EACA,MAAM,UAAU,MAAM,SAAS,QAAQ,kBAAkB,OAAO,GAAG,WAAW;GAC5E,OAAO,MAAM,KAAK,YAAY,WAAW,OAAO,KAAK,IAAI;EAC3D,CAAC;EACD,MAAM,yBAAS,IAAI,IAAwB;EAC3C,KAAK,MAAM,SAAS,SAClB,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,OAAO,IAAI,KAAK,IAAI;EAEvD,OAAO;CACT;CAEA,MAAc,YACZ,WACA,OACA,KACA,MACkC;EAClC,MAAM,SAAS,MAAM,KAAK,UAAU,WAAW,KAAK;EACpD,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAC,UAAU,CAAC,OAAO,uBAAO,IAAI,IAAI;EACtC,MAAM,EAAE,UAAU,QAAQ;EAC1B,MAAM,QAAQ,MAAM,OAAO,WACzB,IAAI,SACJ,MAAM,KAAK,SAAS,KAAK,GAAG,GAC5B,IACF;EACA,MAAM,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,MAAM,SAAS,KAAK,UAAU,OAAO;EAC1E,IAAI,YAAY,MAAM,UAAA,IACpB,MAAM,KAAK,YAAY,aACrB,WACA,SAAS,SAAS,MAClB,UAAU,SAAS,KAAK,CAC1B;EAEF,MAAM,SAAS,MAAM,KAAK,SAAS,WAAW,OAAO,OAAO,GAAG;EAC/D,OAAO,MAAM,KAAK,QAAQ,OAAO,WAAW,QAAQ,IAAI,SAAS,QAAQ,IAAI;CAC/E;;CAGA,MAAc,SACZ,WACA,OACA,OACA,KACkC;EAClC,MAAM,OAAO,MAAM,QAAQ,IACzB,MAAM,IAAI,OAAO,SAAS;GACxB,MAAM,OAAO,MAAM,IAAI,KAAK,IAAI,UAAU,EAAE;GAC5C,MAAM,MACJ,QAAQ,KAAK,UAAU,UAAU,MAAM,KAAK,MAAM,WAAW,MAAM,MAAM,GAAG,IAAI;GAClF,OAAO,MAAO,CAAC,KAAK,KAAK,GAAG,IAAc;EAC5C,CAAC,CACH;EACA,OAAO,IAAI,IAAI,KAAK,QAAQ,UAAU,UAAU,IAAI,CAAC;CACvD;;CAGA,MAAc,UAAU,WAAmB,OAA4C;EACrF,MAAM,QAAQ,MAAM;EACpB,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,EAAE,UAAU,UAAU;EAC5B,MAAM,OAAO,SAAS,SAAS;EAC/B,MAAM,MAAM,UAAA;EACZ,MAAM,aAAa,MACf,MAAM,KAAK,aAAa,YAAY,OAAO,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI,IACjF,MAAM,KAAK,YAAY,IAAI,WAAW,IAAI,CAAC,CAAC,YAAY,IAAI;EAChE,IAAI,CAAC,YAAY,OAAO;EACxB,OAAO,SAAS,OAAO;GACrB;GACA,UAAU,MAAM,UAAU;GAC1B,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC/C,CAAC;CACH;;;;;CAMA,MAAc,SAAS,WAAmB,UAAkB,UAAoC;EAC9F,IAAI,CAAC,YAAY,CAAC,KAAK,eAAgB,MAAM,KAAK,YAAY,KAAK,WAAW,QAAQ,GACpF,OAAA;EAEF,OAAQ,MAAM,KAAK,YAAY,KAAK,UAAU,QAAQ,IAAK,WAAA;CAC7D;;CAGA,MAAc,QACZ,WACA,UACA,KACA,UAC4B;EAC5B,MAAM,SAAS,MAAM,KAAK,OAAO,WAAW,UAAU,KAAA,EAAkB;EACxE,IAAI,UAAU,CAAC,UAAU,OAAO;EAChC,OAAO,MAAM,KAAK,OAAO,WAAW,UAAU,KAAK,QAAQ;CAC7D;CAEA,MAAc,OACZ,WACA,UACA,KACA,OAC4B;EAW5B,QAAO,MAVa,KAAK,GAAG,KAAK,KAAK,OAAO,aAAa;GACxD,OAAO,IACL,GAAG,IAAI,KAAK,OAAO,aAAa,WAAW,GAAG,SAAS,GACvD,GAAG,IAAI,KAAK,OAAO,aAAa,UAAU,GAAG,QAAQ,GACrD,GAAG,IAAI,KAAK,OAAO,aAAa,UAAU,GAAG,KAAK,GAClD,GAAG,IAAI,KAAK,OAAO,aAAa,SAAS,GAAG,IAAI,OAAO,GACvD,GAAG,IAAI,KAAK,OAAO,aAAa,QAAQ,GAAG,IAAI,UAAU,EAAE,CAC7D;GACA,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;;;;;CAOA,MAAc,MACZ,WACA,MACA,MACA,KACqB;EACrB,MAAM,EAAE,KAAK,WAAW;EACxB,MAAM,QAAQ,IAAI,YAAY;EAC9B,MAAM,OAAO,KAAK,UAAU,QAAQ;EACpC,MAAM,SAAS;GACb,MAAM,OAAO,OAAO,OAAO,KAAK;GAChC,SAAS,OAAO,OAAO,UAAU,KAAK;GACtC,cAAc,OAAO,OAAO,eAAe,KAAK;GAChD,OAAO,KAAK;GACZ,WAAW;GACX,WAAW;EACb;EACA,IAAI,CAAC,QACH,OAAQ,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,aAAa;GACpD,IAAI,KAAK;GACT;GACA,UAAU,KAAK,SAAS,SAAS;GACjC,UAAU,KAAK;GACf,SAAS,IAAI;GACb,QAAQ,IAAI,UAAU;GACtB,YAAY;GACZ,eAAe;GACf,WAAW;GACX,GAAG;EACL,CAAC;EAEH,MAAM,aAAa,KAAK,UAAU,aAAa,OAAO,eAAe;EACrE,IAAI,YAAY,MAAM,KAAK,QAAQ,OAAO,MAAM;EAChD,OAAQ,MAAM,KAAK,GAAG,OAAO,KAAK,OAAO,aAAa,OAAO,IAAI;GAC/D,GAAG;GACH,GAAI,aAAa;IAAE,YAAY;IAAM,eAAe;GAAK,IAAI,CAAC;EAChE,CAAC;CACH;AACF;;AAGA,SAAS,MAAM,QAA2B,KAAoB;CAC5D,OAAO,CAAC,UAAU,IAAI,QAAQ,IAAI,KAAK,MAAM,OAAO,SAAS,KAAA;AAC/D;AAEA,SAAS,YAAY,QAA4B,SAA4C;CAC3F,OAAO;EACL,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;CACrD;AACF;AAEA,SAAS,UAAU,OAAoD;CACrE,OAAO,UAAU,kBAAkB,UAAU,cAAc,QAAQ;AACrE;;AAGA,eAAe,KACb,OACA,KACA,MACY;CACZ,MAAM,MAAM,MAAM,IAAI,GAAG;CACzB,IAAI,KAAK,OAAO,MAAM;CACtB,MAAM,UAAU,KAAK;CACrB,MAAM,IAAI,KAAK,OAAO;CACtB,OAAO,MAAM;AACf"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { designRenderPath } from "../paths.mjs";
|
|
5
|
+
//#region src/models/design-renders.ts
|
|
6
|
+
/**
|
|
7
|
+
* Renders live at `{projectId}/design/{provider}/{fileKey}/{node}.png` (a member's own
|
|
8
|
+
* connection: `…/{provider}/user-{id}/{fileKey}/…`), one per
|
|
9
|
+
* linked node and overwritten in place when the file version moves, so storage
|
|
10
|
+
* is bounded by the number of distinct linked nodes. A deleted node drops its
|
|
11
|
+
* render; a link removed from settings (or a deleted project) leaves its PNG behind.
|
|
12
|
+
*/
|
|
13
|
+
var DesignRenderStore = class {
|
|
14
|
+
db;
|
|
15
|
+
table;
|
|
16
|
+
storage;
|
|
17
|
+
logger;
|
|
18
|
+
constructor(db, table, storage, logger) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
this.table = table;
|
|
21
|
+
this.storage = storage;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
/** Cached render bytes of a node, or null when it has none (or storage is not wired). */
|
|
25
|
+
async read(node) {
|
|
26
|
+
if (!this.storage || !node.renderPath) return null;
|
|
27
|
+
return await this.storage.read(node.renderPath).catch(() => null);
|
|
28
|
+
}
|
|
29
|
+
/** Delete a node's stored render (its row is cleared by the caller's update). */
|
|
30
|
+
async remove(node) {
|
|
31
|
+
if (node.renderPath) await this.storage?.delete(node.renderPath).catch(() => {});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fetch renders (one call per file) for the ok node rows that are missing one
|
|
35
|
+
* or hold an older file version, keyed by link URL. Failures keep the previous
|
|
36
|
+
* render and the row unchanged.
|
|
37
|
+
*/
|
|
38
|
+
async ensure(projectId, client, fileKey, stored, call) {
|
|
39
|
+
const needing = [...stored].filter(([, node]) => needsRender(node));
|
|
40
|
+
if (!this.storage || needing.length === 0) return stored;
|
|
41
|
+
try {
|
|
42
|
+
const renders = await client.fetchRenders(fileKey, needing.map(([, node]) => node.nodeId), call);
|
|
43
|
+
const saved = await Promise.all(needing.map(async ([url, node]) => {
|
|
44
|
+
const png = renders.get(node.nodeId);
|
|
45
|
+
return png ? [url, await this.save(projectId, node, png)] : null;
|
|
46
|
+
}));
|
|
47
|
+
for (const entry of saved) if (entry) stored.set(entry[0], entry[1]);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger?.warn({
|
|
50
|
+
err: error,
|
|
51
|
+
fileKey,
|
|
52
|
+
nodes: needing.length
|
|
53
|
+
}, "design render fetch failed");
|
|
54
|
+
}
|
|
55
|
+
return stored;
|
|
56
|
+
}
|
|
57
|
+
async save(projectId, node, png) {
|
|
58
|
+
const path = designRenderPath(projectId, node.provider, node.fileKey, node.nodeId, node.viewerId);
|
|
59
|
+
await this.storage?.write(path, png);
|
|
60
|
+
return await this.db.update(this.table, node.id, {
|
|
61
|
+
renderPath: path,
|
|
62
|
+
renderVersion: node.version,
|
|
63
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/** An ok node row whose render is missing or taken at an older file version. */
|
|
68
|
+
function needsRender(node) {
|
|
69
|
+
return node.state === "ok" && node.nodeId !== "" && node.version !== null && node.renderVersion !== node.version;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { DesignRenderStore };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=design-renders.mjs.map
|