@jr2/orchestrator 0.5.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/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// `@jr2/orchestrator/hooks/gitlab`: jr2's GitLab kind. The `token` verifier: the door compares the digest of the
|
|
2
|
+
// header and removes the header before `read` runs, so this code never sees the token.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { at, defineHookKind, json } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
const user = z.looseObject({ username: z.string() });
|
|
8
|
+
|
|
9
|
+
/** The schema of each event key: the fields that handlers read, in loose objects. */
|
|
10
|
+
export const gitlabEvents = {
|
|
11
|
+
"merge_request.approved": z.looseObject({
|
|
12
|
+
user,
|
|
13
|
+
object_attributes: z.looseObject({ url: z.string(), last_commit: z.looseObject({ id: z.string() }) }),
|
|
14
|
+
}),
|
|
15
|
+
"note.MergeRequest": z.looseObject({
|
|
16
|
+
user,
|
|
17
|
+
object_attributes: z.looseObject({ note: z.string() }),
|
|
18
|
+
merge_request: z.looseObject({ url: z.string() }),
|
|
19
|
+
}),
|
|
20
|
+
"pipeline.success": z.looseObject({
|
|
21
|
+
object_attributes: z.looseObject({ sha: z.string(), status: z.string() }),
|
|
22
|
+
}),
|
|
23
|
+
"pipeline.failed": z.looseObject({
|
|
24
|
+
object_attributes: z.looseObject({ sha: z.string(), status: z.string() }),
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const text = (v: unknown): string | undefined => (typeof v === "string" && v !== "" ? v : undefined);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A GitLab project or group webhook.
|
|
32
|
+
* - verifier: `token`, the secret token in `X-Gitlab-Token`.
|
|
33
|
+
* - handshake: none. A "Test" delivery from GitLab's settings is an ordinary delivery.
|
|
34
|
+
* - event key: `<object_kind>.<the second part>`, or `<object_kind>` when it has no second part.
|
|
35
|
+
* The second part is `object_attributes.noteable_type` for a `note`, and else
|
|
36
|
+
* `object_attributes.action`, then `object_attributes.status`: `merge_request.approved`,
|
|
37
|
+
* `note.MergeRequest`, `pipeline.success`.
|
|
38
|
+
* - delivery id: `Idempotency-Key` (the same on a retry), else `X-Gitlab-Event-UUID`. (From
|
|
39
|
+
* GitLab's docs; not checked against a real delivery.)
|
|
40
|
+
* - `onBehalfOf`: `user.username`.
|
|
41
|
+
* - answer: the outcome (the default). No person reads it.
|
|
42
|
+
*/
|
|
43
|
+
export const gitlab = defineHookKind({
|
|
44
|
+
api: 1,
|
|
45
|
+
id: "gitlab",
|
|
46
|
+
events: gitlabEvents,
|
|
47
|
+
verify: { token: { header: "x-gitlab-token" } },
|
|
48
|
+
read: (req) => {
|
|
49
|
+
const p = json(req);
|
|
50
|
+
const kind = text(at(p, "object_kind"));
|
|
51
|
+
if (kind === undefined) throw new Error("the body has no object_kind");
|
|
52
|
+
const second =
|
|
53
|
+
kind === "note"
|
|
54
|
+
? text(at(p, "object_attributes.noteable_type"))
|
|
55
|
+
: (text(at(p, "object_attributes.action")) ?? text(at(p, "object_attributes.status")));
|
|
56
|
+
const id = text(req.headers["idempotency-key"]) ?? text(req.headers["x-gitlab-event-uuid"]);
|
|
57
|
+
if (id === undefined) throw new Error("no Idempotency-Key or X-Gitlab-Event-UUID header");
|
|
58
|
+
const by = text(at(p, "user.username"));
|
|
59
|
+
return {
|
|
60
|
+
key: second ? `${kind}.${second}` : kind,
|
|
61
|
+
id,
|
|
62
|
+
payload: p,
|
|
63
|
+
...(by ? { onBehalfOf: by } : {}),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
declare module "./index.ts" {
|
|
69
|
+
interface HookKinds {
|
|
70
|
+
gitlab: typeof gitlab;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The key that a Hook value keeps its parts under. Internal: `defineHookKind` (./index.ts) writes
|
|
2
|
+
// it and the door (./door.ts) reads it. A kind never imports this module.
|
|
3
|
+
//
|
|
4
|
+
// `Symbol.for`, so that the key is the same in each copy of this module. The Kit image holds one
|
|
5
|
+
// copy of `@jr2/orchestrator` (ADR-0069), so a second copy is a fault, but the key does not depend on it.
|
|
6
|
+
|
|
7
|
+
export const HOOK: unique symbol = Symbol.for("@jr2/orchestrator/hooks.hook");
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// `@jr2/orchestrator/hooks`: the interface between the Hook's door (jr2) and a Hook kind (ADR-0078).
|
|
2
|
+
//
|
|
3
|
+
// A Hook kind is one outside service's protocol, as a plain value: data, and pure, synchronous
|
|
4
|
+
// functions. jr2's own kinds (`@jr2/orchestrator/hooks/github` and the others) use this interface and nothing
|
|
5
|
+
// more, so a kind from another package can do all that they do.
|
|
6
|
+
//
|
|
7
|
+
// THE DOOR (jr2, the same for every kind) THE KIND (one for each service)
|
|
8
|
+
// POST /hooks/:name, 401, the hook log what the service signed, and the signature
|
|
9
|
+
// the repeat check (id AND digest) the service's timestamp, if it is signed
|
|
10
|
+
// the clock and the window (300 s) the decoding of the body (JSON, form)
|
|
11
|
+
// RUNS the verifier: `mac` in the Instance the answer to a handshake
|
|
12
|
+
// Effector, `token` and `ed25519` in the the event key, the delivery id, `onBehalfOf`
|
|
13
|
+
// Orchestrator a zod schema for each event key
|
|
14
|
+
// RUNS the parse of the payload who reads the answer: the service or a person
|
|
15
|
+
// the fan-out to Gates and Sources the envelope of a text that a person reads
|
|
16
|
+
// `reply`: the join, the skip rule, the the limit on the text, and the hosts of a
|
|
17
|
+
// door's own text, the cut to `maxText` follow-up
|
|
18
|
+
// the follow-up POST (Instance Effector `http`)
|
|
19
|
+
//
|
|
20
|
+
// The rules:
|
|
21
|
+
// 1. A kind never gets a credential. jr2 makes the kind's constructor (`defineHookKind`), so the
|
|
22
|
+
// credential goes from the config to the door and never through code of the kind. For
|
|
23
|
+
// `token`, the door removes the token's header before it calls the kind.
|
|
24
|
+
// 2. A kind is pure and synchronous: no I/O, no Promise, no clock. The door owns the time.
|
|
25
|
+
// 3. `jr2.config.ts` declares a kind by `import`, and `jr2 up` bundles it into the Assembly. jr2
|
|
26
|
+
// finds no kind in a folder and loads none at run time.
|
|
27
|
+
// 4. A kind adds its id and its payloads to `HookKinds` (below), so the `hooks` of a Gate or a
|
|
28
|
+
// Source are typed by kind id and event key (ADR-0011).
|
|
29
|
+
// 5. A kind gives a zod schema for each event key. The door parses each payload with it, after
|
|
30
|
+
// the verifier and before the fan-out, and a handler's payload has the schema's output type.
|
|
31
|
+
|
|
32
|
+
import type { z } from "zod";
|
|
33
|
+
import { HOOK } from "./hook.ts";
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// 1. What the door gives a kind: the request, and nothing more.
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One delivery, as the door got it. All of it is data. Header names are lowercase.
|
|
41
|
+
*
|
|
42
|
+
* For a kind with a `token` verifier, `headers` does not hold the token's header: the door removes
|
|
43
|
+
* it before it calls the kind. For `mac` and `ed25519`, no part of the request is secret: a
|
|
44
|
+
* signature cannot make another signature.
|
|
45
|
+
*/
|
|
46
|
+
export type HookRequest = {
|
|
47
|
+
readonly headers: { readonly [name: string]: string | undefined };
|
|
48
|
+
/** The raw body. A `mac` or `ed25519` signature covers these bytes. */
|
|
49
|
+
readonly body: Uint8Array;
|
|
50
|
+
/** The raw body as UTF-8. */
|
|
51
|
+
readonly text: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** A JSON value: what a kind puts in an answer. */
|
|
55
|
+
export type Json = null | boolean | number | string | readonly Json[] | { readonly [k: string]: Json };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The body as JSON. It throws when the body is not JSON. A throw from `read` is a 400, and the
|
|
59
|
+
* hook log keeps the error.
|
|
60
|
+
*/
|
|
61
|
+
export function json(req: HookRequest): unknown {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(req.text) as unknown;
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error("the body is not JSON");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The body as `application/x-www-form-urlencoded` fields. When a field occurs more than one time,
|
|
71
|
+
* the first value is the field's value.
|
|
72
|
+
*/
|
|
73
|
+
export function form(req: HookRequest): { readonly [field: string]: string | undefined } {
|
|
74
|
+
const out: { [field: string]: string } = Object.create(null) as { [field: string]: string };
|
|
75
|
+
for (const [name, value] of new URLSearchParams(req.text)) {
|
|
76
|
+
if (!Object.hasOwn(out, name)) out[name] = value;
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze(out);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The value at a dotted path (`sender.login`, `actions.0.action_id`) of a decoded body, or
|
|
83
|
+
* `undefined`. It reads own properties only.
|
|
84
|
+
*/
|
|
85
|
+
export function at(value: unknown, path: string): unknown {
|
|
86
|
+
let here: unknown = value;
|
|
87
|
+
for (const step of path.split(".")) {
|
|
88
|
+
if (here === null || typeof here !== "object" || !Object.hasOwn(here, step)) return undefined;
|
|
89
|
+
here = (here as { readonly [k: string]: unknown })[step];
|
|
90
|
+
}
|
|
91
|
+
return here;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
// 2. The verifier: a closed set of three (ADR-0078). The kind says WHAT to check; the door checks.
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* What the service signed, from one request.
|
|
100
|
+
*
|
|
101
|
+
* The door makes the checks. For `mac`, it first checks `at` against its clock, then sends the
|
|
102
|
+
* bytes, the signature, the algorithm and the NAME of the Hook's secret to the Instance Effector,
|
|
103
|
+
* which answers "match" or "no match" (ADR-0068). For `ed25519`, it checks `at`, then the
|
|
104
|
+
* signature against the Hook's public key, in the Orchestrator.
|
|
105
|
+
*/
|
|
106
|
+
export type Signed = {
|
|
107
|
+
/** The bytes that the service signed, in sequence. The door joins them. A string is UTF-8.
|
|
108
|
+
* GitHub: `[req.body]`. Slack: `["v0:", timestamp, ":", req.body]`. */
|
|
109
|
+
readonly bytes: readonly (string | Uint8Array)[];
|
|
110
|
+
/** The signature, with no prefix (`sha256=`, `v0=`): the kind removes the prefix. */
|
|
111
|
+
readonly signature: { readonly hex: string } | { readonly base64: string };
|
|
112
|
+
/**
|
|
113
|
+
* The service's time of the delivery, in seconds since the epoch. Give it ONLY when the
|
|
114
|
+
* signature covers it (Slack, Discord). The door refuses a delivery whose `at` is more than
|
|
115
|
+
* 300 s from its clock: 401. A kind cannot change the window.
|
|
116
|
+
*/
|
|
117
|
+
readonly at?: number;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** The request does not hold what the verifier needs. The door answers 401 and logs `unsigned`. */
|
|
121
|
+
export type Unsigned = { readonly unsigned: string };
|
|
122
|
+
|
|
123
|
+
/** The HMAC algorithms that a `mac` verifier can name. */
|
|
124
|
+
export type MacAlg = "sha256" | "sha1" | "sha512";
|
|
125
|
+
|
|
126
|
+
/** How the service signs. One of three. */
|
|
127
|
+
export type Verify =
|
|
128
|
+
/** An HMAC that a shared secret makes. Only the Instance Effector holds the secret. */
|
|
129
|
+
| {
|
|
130
|
+
readonly mac: {
|
|
131
|
+
readonly alg: MacAlg;
|
|
132
|
+
readonly signed: (req: HookRequest) => Signed | Unsigned;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** An Ed25519 signature that the service's private key makes. The Hook's config holds the PUBLIC
|
|
136
|
+
* key as a literal: it can check a signature and cannot make one. */
|
|
137
|
+
| { readonly ed25519: { readonly signed: (req: HookRequest) => Signed | Unsigned } }
|
|
138
|
+
/** A shared token in a header (GitLab). The door compares its sha-256 with the digest that
|
|
139
|
+
* `jr2 up` gave it, then removes the header. The kind never reads the token. */
|
|
140
|
+
| { readonly token: { readonly header: string } };
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A named secret: the value of `secret("NAME")` from `@jr2/orchestrator`. The config holds the
|
|
144
|
+
* name only. The value is in the Instance Effector (`mac`), or its digest is in the Orchestrator
|
|
145
|
+
* (`token`).
|
|
146
|
+
*/
|
|
147
|
+
export type HookSecret = { readonly secret: string };
|
|
148
|
+
|
|
149
|
+
/** What the Hook's config gives for each verifier. The kind never gets it. */
|
|
150
|
+
export type CredentialOf<V extends Verify> = V extends { readonly ed25519: unknown }
|
|
151
|
+
? { readonly publicKey: string }
|
|
152
|
+
: { readonly secret: HookSecret };
|
|
153
|
+
|
|
154
|
+
/** The credential of a Hook, for each of the three verifiers. */
|
|
155
|
+
export type HookCredential = { readonly secret: HookSecret } | { readonly publicKey: string };
|
|
156
|
+
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// 3. What a kind reads from a verified request: a handshake, or a delivery.
|
|
159
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* An answer that the kind writes. Its status is always 200: the door owns every other status.
|
|
163
|
+
* `body`: an object or an array is JSON, a string is `text/plain`, `null` is an empty body.
|
|
164
|
+
* `then`: one POST that the door makes AFTER the answer, through the Instance Effector's `http`,
|
|
165
|
+
* with no secret, one time (the `response_url` of a Slack click). Its url must start with one of
|
|
166
|
+
* the kind's `posts`. A failed post is a line in the hook log.
|
|
167
|
+
*/
|
|
168
|
+
export type Answer = {
|
|
169
|
+
readonly body: Json;
|
|
170
|
+
readonly then?: { readonly post: `https://${string}`; readonly json: Json };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** The service's handshake (Slack `url_verification`, Discord `PING`, GitHub `ping`). It comes
|
|
174
|
+
* after the verifier and gets this answer. It reaches no handler, and the hook log keeps it as
|
|
175
|
+
* `handshake`. */
|
|
176
|
+
export type Handshake = { readonly handshake: Answer };
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* One delivery, which the door gives to the Gates and the Sources whose handlers hear its kind and
|
|
180
|
+
* `key`. The kind's `events` gives the schema for each key. A key that `events` does not name is
|
|
181
|
+
* still a delivery: the door does not parse it, and no handler can hear it, so it reaches nothing.
|
|
182
|
+
*/
|
|
183
|
+
export type Delivery = {
|
|
184
|
+
/** The event key. GitHub: `pull_request_review.submitted`. */
|
|
185
|
+
readonly key: string;
|
|
186
|
+
/** The delivery id. The door's repeat check uses it AND the digest of the body. */
|
|
187
|
+
readonly id: string;
|
|
188
|
+
/** The decoded body. The signature covers it. The door parses it with the kind's schema for
|
|
189
|
+
* `key` (see `events`), and a handler gets the schema's output. */
|
|
190
|
+
readonly payload: unknown;
|
|
191
|
+
/** The party that the service says acted. The signature covers it, so jr2 marks it checked. */
|
|
192
|
+
readonly onBehalfOf?: string;
|
|
193
|
+
/**
|
|
194
|
+
* Who reads the answer. `"service"` (the default): the answer is the outcome JSON with the
|
|
195
|
+
* status that ADR-0011 gives (429, 503). `"person"` (a chat command or a click): the door joins
|
|
196
|
+
* the `reply` texts, cuts them to `maxText`, and calls the kind's `answer`. The status is 200.
|
|
197
|
+
*/
|
|
198
|
+
readonly readBy?: "service" | "person";
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// 4. The kind.
|
|
203
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/** The version of this interface. jr2 1.x calls each kind of `api: 1` as this module says. */
|
|
206
|
+
export type HookKindApi = 1;
|
|
207
|
+
|
|
208
|
+
/** The versions of the interface that this copy of `@jr2/orchestrator/hooks` knows. */
|
|
209
|
+
export const HOOK_KIND_APIS: readonly HookKindApi[] = Object.freeze([1]);
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The schema of each event key: `{ "pull_request_review.submitted": z.looseObject({ … }) }`.
|
|
213
|
+
*
|
|
214
|
+
* - The door parses the payload of a delivery with the schema of its key, after the verifier and
|
|
215
|
+
* before the fan-out. A failed parse reaches no handler: the hook log keeps a `payload-refused`
|
|
216
|
+
* line with zod's issues, and the answer names the kind and the key. The status is 200, because
|
|
217
|
+
* a second try fails in the same way.
|
|
218
|
+
* - Use `z.looseObject` for each object. A loose object keeps the fields that it does not name, so
|
|
219
|
+
* a new field from the service breaks nothing. A named field that is missing, or that has the
|
|
220
|
+
* wrong type, fails the parse. Name only the fields that handlers need. A value that the service
|
|
221
|
+
* can extend (a review's `state`) is a string, not an enum.
|
|
222
|
+
* - A key that ends in `.*` is the schema of each key with that prefix: `"block_actions.*"` types
|
|
223
|
+
* `block_actions.approve` and `block_actions.reject`. An exact key comes first, then the longest
|
|
224
|
+
* prefix.
|
|
225
|
+
*/
|
|
226
|
+
export type Events = { readonly [key: string]: z.ZodType };
|
|
227
|
+
|
|
228
|
+
/** A key of `events` as a handler's key: `"block_actions.*"` → `` `block_actions.${string}` ``. */
|
|
229
|
+
export type HandlerKey<K> = K extends `${infer P}.*` ? `${P}.${string}` : K;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The schema's output with only its named fields. The output of a loose object also has
|
|
233
|
+
* `[field: string]: unknown`. The fields that it does not name stay in the value, but this type
|
|
234
|
+
* removes the index signature, so a handler cannot read a field that its schema does not name.
|
|
235
|
+
*/
|
|
236
|
+
export type Named<T> = T extends readonly unknown[]
|
|
237
|
+
? { readonly [I in keyof T]: Named<T[I]> }
|
|
238
|
+
: T extends object
|
|
239
|
+
? { readonly [K in keyof T as string extends K ? never : K]: Named<T[K]> }
|
|
240
|
+
: T;
|
|
241
|
+
|
|
242
|
+
/** The payload type of each handler key of `events`. */
|
|
243
|
+
export type Payloads<E extends Events> = {
|
|
244
|
+
readonly [K in keyof E & string as HandlerKey<K>]: Named<z.output<E[K]>>;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* A Hook kind: one outside service's protocol.
|
|
249
|
+
*
|
|
250
|
+
* - `api`: the version of this interface that the kind was written for. Only `1` exists.
|
|
251
|
+
* - `id`: the key of the `hooks` of a Gate or a Source. One id is one kind in an Assembly.
|
|
252
|
+
* Lowercase letters, digits and `-`, and it starts with a letter.
|
|
253
|
+
* - `events`: a zod schema for each event key that a handler can hear ({@link Events}).
|
|
254
|
+
* - `verify`: one of the three verifiers, and what the service signed.
|
|
255
|
+
* - `read`: the verified request, as a handshake or a delivery. A throw is 400, and the hook log
|
|
256
|
+
* keeps the error.
|
|
257
|
+
* - `answer`: the envelope of the text for a delivery with `readBy: "person"`. The door gives the
|
|
258
|
+
* joined text, already cut to `maxText`. The kind escapes it. A kind with no `answer` cannot give
|
|
259
|
+
* `readBy: "person"`: the type of `read` refuses it ({@link defineHookKind}).
|
|
260
|
+
* - `maxText`: the service's limit on the length of the text (Discord: 2000).
|
|
261
|
+
* - `posts`: the url prefixes of `Answer.then`. `jr2 up` adds them to the Instance Effector's fence
|
|
262
|
+
* for each Hook of this kind (ADR-0068). The door refuses a `then` to another url.
|
|
263
|
+
*/
|
|
264
|
+
export type HookKind<Id extends string = string, E extends Events = Events, V extends Verify = Verify> = {
|
|
265
|
+
readonly api: HookKindApi;
|
|
266
|
+
readonly id: Id;
|
|
267
|
+
readonly events: E;
|
|
268
|
+
readonly verify: V;
|
|
269
|
+
readonly read: (req: HookRequest) => Handshake | Delivery;
|
|
270
|
+
readonly answer?: (text: string, delivery: Delivery) => Answer;
|
|
271
|
+
readonly maxText?: number;
|
|
272
|
+
readonly posts?: readonly `https://${string}/`[];
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** The Workflows whose Gates a Hook may answer (ADR-0078). The default is `"all"`. A Hook never
|
|
276
|
+
* starts a run. */
|
|
277
|
+
export type HookMay = { readonly answer: "all" | readonly string[] };
|
|
278
|
+
|
|
279
|
+
/** The parts of a Hook, which only the door reads. */
|
|
280
|
+
export type HookParts<K extends HookKind = HookKind> = {
|
|
281
|
+
readonly kind: K;
|
|
282
|
+
readonly credential: HookCredential;
|
|
283
|
+
readonly may: HookMay;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A Hook: a kind, a credential and `may`. It is the value of one entry of `hooks` in
|
|
288
|
+
* `jr2.config.ts`, and the entry's key is its name, the last segment of `POST /hooks/:name`. Only
|
|
289
|
+
* the door reads it.
|
|
290
|
+
*/
|
|
291
|
+
export type Hook<K extends HookKind = HookKind> = { readonly [HOOK]: HookParts<K> };
|
|
292
|
+
|
|
293
|
+
/** A kind is also the constructor of its Hooks: `github({ secret: secret("GH_HOOK"), may })`. */
|
|
294
|
+
export type HookKindOf<Id extends string, E extends Events, V extends Verify> = HookKind<Id, E, V> &
|
|
295
|
+
((hook: CredentialOf<V> & { readonly may?: HookMay }) => Hook<HookKind<Id, E, V>>);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Define a kind. jr2 makes the constructor, so the credential goes from the config to the door and
|
|
299
|
+
* never through code of the kind.
|
|
300
|
+
*
|
|
301
|
+
* It throws on an `api` that it does not know, an `id` that is not lowercase, an `events` entry
|
|
302
|
+
* that is not a schema, a verifier that is not one of the three, a `posts` entry that is not an
|
|
303
|
+
* `https://` url that ends in `/`, and a `maxText` that is not a positive integer. Two different
|
|
304
|
+
* kinds with one `id` fail the typecheck (`HookKinds`) and `jr2 up`.
|
|
305
|
+
*
|
|
306
|
+
* Two overloads: a kind with `answer` can give `readBy: "person"`; a kind with no `answer` cannot.
|
|
307
|
+
*/
|
|
308
|
+
export function defineHookKind<const Id extends string, const E extends Events, const V extends Verify>(
|
|
309
|
+
kind: HookKind<Id, E, V> & { readonly answer: NonNullable<HookKind["answer"]> },
|
|
310
|
+
): HookKindOf<Id, E, V>;
|
|
311
|
+
export function defineHookKind<const Id extends string, const E extends Events, const V extends Verify>(
|
|
312
|
+
kind: Omit<HookKind<Id, E, V>, "read" | "answer" | "maxText" | "posts"> & {
|
|
313
|
+
readonly read: (req: HookRequest) => Handshake | (Delivery & { readonly readBy?: "service" });
|
|
314
|
+
},
|
|
315
|
+
): HookKindOf<Id, E, V>;
|
|
316
|
+
export function defineHookKind(kind: HookKind): HookKindOf<string, Events, Verify> {
|
|
317
|
+
checkKind(kind);
|
|
318
|
+
const make = (hook: unknown): Hook => {
|
|
319
|
+
const parts: HookParts = Object.freeze({
|
|
320
|
+
kind: made,
|
|
321
|
+
credential: credentialOf(kind, hook),
|
|
322
|
+
may: mayOf(kind.id, (hook as { readonly may?: unknown } | undefined)?.may),
|
|
323
|
+
});
|
|
324
|
+
return Object.freeze({ [HOOK]: parts });
|
|
325
|
+
};
|
|
326
|
+
const fields: HookKind = {
|
|
327
|
+
api: kind.api,
|
|
328
|
+
id: kind.id,
|
|
329
|
+
events: Object.freeze({ ...kind.events }),
|
|
330
|
+
verify: Object.freeze({ ...kind.verify }),
|
|
331
|
+
read: kind.read,
|
|
332
|
+
...(kind.answer ? { answer: kind.answer } : {}),
|
|
333
|
+
...(kind.maxText !== undefined ? { maxText: kind.maxText } : {}),
|
|
334
|
+
...(kind.posts ? { posts: Object.freeze([...kind.posts]) } : {}),
|
|
335
|
+
};
|
|
336
|
+
const made = Object.freeze(Object.assign(make, fields)) as unknown as HookKindOf<string, Events, Verify>;
|
|
337
|
+
return made;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The lowercase id of a kind. */
|
|
341
|
+
const KIND_ID = /^[a-z][a-z0-9-]*$/;
|
|
342
|
+
|
|
343
|
+
function checkKind(kind: HookKind): void {
|
|
344
|
+
const id: unknown = kind.id;
|
|
345
|
+
if (typeof id !== "string" || !KIND_ID.test(id)) {
|
|
346
|
+
throw new Error(`a Hook kind's id is lowercase letters, digits and "-", and starts with a letter: ${String(id)}`);
|
|
347
|
+
}
|
|
348
|
+
if (!HOOK_KIND_APIS.includes(kind.api)) {
|
|
349
|
+
throw new Error(`Hook kind ${id} states api ${String(kind.api)}; this @jr2/orchestrator/hooks knows api 1 only`);
|
|
350
|
+
}
|
|
351
|
+
if (kind.events === null || typeof kind.events !== "object") throw new Error(`Hook kind ${id} has no events`);
|
|
352
|
+
for (const [key, schema] of Object.entries(kind.events)) {
|
|
353
|
+
if (typeof (schema as { safeParse?: unknown } | null)?.safeParse !== "function") {
|
|
354
|
+
throw new Error(`Hook kind ${id}: the event key ${key} has no zod schema`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const verifiers = ["mac", "ed25519", "token"].filter((v) => Object.hasOwn(kind.verify ?? {}, v));
|
|
358
|
+
if (verifiers.length !== 1) {
|
|
359
|
+
throw new Error(`Hook kind ${id}: verify is one of mac, ed25519 or token`);
|
|
360
|
+
}
|
|
361
|
+
const v = kind.verify as {
|
|
362
|
+
mac?: { alg?: unknown; signed?: unknown };
|
|
363
|
+
ed25519?: { signed?: unknown };
|
|
364
|
+
token?: { header?: unknown };
|
|
365
|
+
};
|
|
366
|
+
if (v.mac && (!["sha256", "sha1", "sha512"].includes(v.mac.alg as string) || typeof v.mac.signed !== "function")) {
|
|
367
|
+
throw new Error(`Hook kind ${id}: a mac verifier has an alg (sha256, sha1, sha512) and a signed function`);
|
|
368
|
+
}
|
|
369
|
+
if (v.ed25519 && typeof v.ed25519.signed !== "function") {
|
|
370
|
+
throw new Error(`Hook kind ${id}: an ed25519 verifier has a signed function`);
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
v.token &&
|
|
374
|
+
(typeof v.token.header !== "string" || v.token.header !== v.token.header.toLowerCase() || v.token.header === "")
|
|
375
|
+
) {
|
|
376
|
+
throw new Error(`Hook kind ${id}: a token verifier names its header in lowercase`);
|
|
377
|
+
}
|
|
378
|
+
if (typeof kind.read !== "function") throw new Error(`Hook kind ${id} has no read function`);
|
|
379
|
+
if (kind.answer !== undefined && typeof kind.answer !== "function") {
|
|
380
|
+
throw new Error(`Hook kind ${id}: answer is a function`);
|
|
381
|
+
}
|
|
382
|
+
if (kind.maxText !== undefined && !(Number.isInteger(kind.maxText) && kind.maxText > 0)) {
|
|
383
|
+
throw new Error(`Hook kind ${id}: maxText is a positive integer`);
|
|
384
|
+
}
|
|
385
|
+
for (const post of kind.posts ?? []) {
|
|
386
|
+
let url: URL | undefined;
|
|
387
|
+
try {
|
|
388
|
+
url = new URL(post);
|
|
389
|
+
} catch {
|
|
390
|
+
url = undefined;
|
|
391
|
+
}
|
|
392
|
+
if (!url || url.protocol !== "https:" || !post.endsWith("/") || url.href !== post) {
|
|
393
|
+
throw new Error(`Hook kind ${id}: a posts entry is an https:// url that ends in "/": ${post}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** The public key of an Ed25519 verifier: 32 bytes in hex, as Discord shows it. */
|
|
399
|
+
const PUBLIC_KEY = /^[0-9a-f]{64}$/i;
|
|
400
|
+
|
|
401
|
+
function credentialOf(kind: HookKind, hook: unknown): HookCredential {
|
|
402
|
+
const h = (hook ?? {}) as { readonly secret?: unknown; readonly publicKey?: unknown };
|
|
403
|
+
if ("ed25519" in kind.verify) {
|
|
404
|
+
if (typeof h.publicKey !== "string" || !PUBLIC_KEY.test(h.publicKey)) {
|
|
405
|
+
throw new Error(`a ${kind.id} Hook takes publicKey: the Ed25519 public key in hex (64 characters)`);
|
|
406
|
+
}
|
|
407
|
+
return Object.freeze({ publicKey: h.publicKey.toLowerCase() });
|
|
408
|
+
}
|
|
409
|
+
const s = h.secret as { readonly secret?: unknown } | undefined;
|
|
410
|
+
if (typeof s?.secret !== "string" || s.secret === "") {
|
|
411
|
+
throw new Error(`a ${kind.id} Hook takes secret: a named secret, secret("NAME")`);
|
|
412
|
+
}
|
|
413
|
+
return Object.freeze({ secret: s as HookSecret });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function mayOf(id: string, may: unknown): HookMay {
|
|
417
|
+
if (may === undefined) return Object.freeze({ answer: "all" });
|
|
418
|
+
const answer = (may as { readonly answer?: unknown } | null)?.answer;
|
|
419
|
+
if (answer === "all") return Object.freeze({ answer });
|
|
420
|
+
if (Array.isArray(answer) && answer.every((w) => typeof w === "string" && w !== "")) {
|
|
421
|
+
return Object.freeze({ answer: Object.freeze([...(answer as string[])]) });
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`a ${id} Hook's may.answer is "all" or a list of Workflow names`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
427
|
+
// 5. The registry of TYPES: kind id → kind. Each kind module adds itself:
|
|
428
|
+
// declare module "./index.ts" { interface HookKinds { github: typeof github } }
|
|
429
|
+
// There is no registry at run time. Two kinds that give one id with different types do not
|
|
430
|
+
// compile ("Subsequent property declarations must have the same type").
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/** Each kind module adds its id and its kind here by declaration merging. */
|
|
434
|
+
// biome-ignore lint: an interface for declaration merging.
|
|
435
|
+
export interface HookKinds {}
|
|
436
|
+
|
|
437
|
+
/** The payload map of a kind: handler key → the output of its schema. */
|
|
438
|
+
export type EventsOf<K> = K extends HookKind<string, infer E, Verify> ? Payloads<E> : never;
|
|
439
|
+
|
|
440
|
+
/** A kind's `events` keys with `*`, which take no handler: the door matches a key exactly (ADR-0011). */
|
|
441
|
+
export type NoWildcardKeys<K> =
|
|
442
|
+
K extends HookKind<string, infer E, Verify>
|
|
443
|
+
? { readonly [Key in keyof E & string as Key extends `${string}*${string}` ? Key : never]?: never }
|
|
444
|
+
: never;
|
|
445
|
+
|
|
446
|
+
/** The payload of one handler key of a kind in `HookKinds`: `PayloadOf<"github", "issue_comment.created">`.
|
|
447
|
+
* A forge part types its handlers with it. */
|
|
448
|
+
export type PayloadOf<Id extends keyof HookKinds, Key extends keyof EventsOf<HookKinds[Id]>> = EventsOf<
|
|
449
|
+
HookKinds[Id]
|
|
450
|
+
>[Key];
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Handlers by kind id, then by event key. An entry of `HookKinds` whose key is not its kind's `id`
|
|
454
|
+
* is left out, so no handler is keyed by a name that the door never matches. `In` is what the Gate
|
|
455
|
+
* or the Source gives a handler beside `payload`; `Out` is what the handler gives. A key that the
|
|
456
|
+
* kind's payloads do not name is an excess property: it does not compile. A key that holds `*`
|
|
457
|
+
* does not compile ({@link NoWildcardKeys}).
|
|
458
|
+
*/
|
|
459
|
+
export type KindHooks<In, Out> = {
|
|
460
|
+
readonly [Id in keyof HookKinds as HookKinds[Id] extends { readonly id: Id } ? Id : never]?: {
|
|
461
|
+
readonly [Key in keyof EventsOf<HookKinds[Id]>]?: (
|
|
462
|
+
d: In & { readonly payload: EventsOf<HookKinds[Id]>[Key] },
|
|
463
|
+
) => Out;
|
|
464
|
+
} & NoWildcardKeys<HookKinds[Id]>;
|
|
465
|
+
};
|