@rse/ase 1.0.6 → 2.0.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/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import Hapi from "@hapi/hapi";
|
|
7
|
+
import { WebSocketServer } from "ws";
|
|
8
|
+
import * as Core from "./ase-task-store-core.js";
|
|
9
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
10
|
+
import { AgentRelayBinding } from "./ase-agent-relay-server-bind.js";
|
|
11
|
+
import { ServerGuard, urlHost } from "./ase-lib-server-bind.js";
|
|
12
|
+
/* the route path templates and the JSON payload route options */
|
|
13
|
+
const P = "/projects/{prjId}";
|
|
14
|
+
const T = `${P}/tasks/{taskId}`;
|
|
15
|
+
const json = { payload: { parse: true, allow: "application/json" } };
|
|
16
|
+
/* the interval of the keepalive pings of the subscribers, the maximum size of the
|
|
17
|
+
messages of the subscribers (which send none at all), the maximum amount of not
|
|
18
|
+
yet transmitted data of a subscriber (beyond which it cannot keep up, generously
|
|
19
|
+
bounded as the frames carry entire task plans), and the maximum number of
|
|
20
|
+
subscribers per project and in total */
|
|
21
|
+
const PING_MS = 15 * 1000;
|
|
22
|
+
const PAYLOAD_MAX = 4 * 1024;
|
|
23
|
+
const BUFFER_MAX = 16 * 1024 * 1024;
|
|
24
|
+
const SUBS_MAX = 128;
|
|
25
|
+
const SUBS_TOTAL_MAX = 1024;
|
|
26
|
+
/* the HTTP binding of the REST API: bearer token authentication, CORS,
|
|
27
|
+
the routes onto the core operations, problem details rendering, and
|
|
28
|
+
the WebSocket event endpoint, optionally co-hosting the agent relay */
|
|
29
|
+
export class TaskStoreServer {
|
|
30
|
+
log;
|
|
31
|
+
options;
|
|
32
|
+
server;
|
|
33
|
+
wss;
|
|
34
|
+
core;
|
|
35
|
+
subs = new Map();
|
|
36
|
+
guard;
|
|
37
|
+
relay;
|
|
38
|
+
unanswered = new WeakSet();
|
|
39
|
+
timer = null;
|
|
40
|
+
constructor(log, options) {
|
|
41
|
+
this.log = log;
|
|
42
|
+
this.options = options;
|
|
43
|
+
this.relay = options.relay ? new AgentRelayBinding(log) : null;
|
|
44
|
+
this.core = new Core.TaskStoreCore(options.store, (prjId, frame) => this.emit(prjId, frame));
|
|
45
|
+
this.server = Hapi.server({
|
|
46
|
+
host: options.address,
|
|
47
|
+
port: options.port,
|
|
48
|
+
tls: options.tls ?? undefined,
|
|
49
|
+
routes: {
|
|
50
|
+
cors: options.cors.length === 0 ? false : {
|
|
51
|
+
origin: options.cors,
|
|
52
|
+
headers: ["Authorization", "Content-Type", "If-None-Match", "If-Match"],
|
|
53
|
+
exposedHeaders: ["Location", "ETag"],
|
|
54
|
+
maxAge: 86400
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
this.wss = new WebSocketServer({ noServer: true, maxPayload: PAYLOAD_MAX });
|
|
59
|
+
/* install the common extensions, logging the requests of the co-hosted
|
|
60
|
+
agent relay at debug level only (as the agents announce frequently) */
|
|
61
|
+
this.guard = new ServerGuard(log, {
|
|
62
|
+
label: "task store",
|
|
63
|
+
token: options.token,
|
|
64
|
+
failure: "internal task store failure (see service log for details)",
|
|
65
|
+
level: (path) => this.relay !== null && AgentRelayBinding.handles(path) ? "debug" : "info",
|
|
66
|
+
upgrade: (req, socket, head) => this.upgrade(req, socket, head)
|
|
67
|
+
});
|
|
68
|
+
this.guard.install(this.server);
|
|
69
|
+
this.installRoutes();
|
|
70
|
+
}
|
|
71
|
+
/* start the server: open the storage, start the co-hosted agent relay,
|
|
72
|
+
bind the listener, and start the keepalive pings of the subscribers */
|
|
73
|
+
async start() {
|
|
74
|
+
await this.options.store.open();
|
|
75
|
+
this.relay?.start();
|
|
76
|
+
try {
|
|
77
|
+
await this.server.start();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
/* ignore a close failure to not mask the original start failure */
|
|
81
|
+
this.relay?.stop();
|
|
82
|
+
await this.options.store.close().catch(() => { });
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
this.timer = setInterval(() => { this.keepalive(); }, PING_MS);
|
|
86
|
+
const scheme = this.options.tls !== null ? "https" : "http";
|
|
87
|
+
this.log.write("info", `task store: listening on ${scheme}://${urlHost(this.options.address)}:${this.options.port} ` +
|
|
88
|
+
`(storage plugin "${this.options.store.name}"${this.relay !== null ? ", with agent relay" : ""})`);
|
|
89
|
+
}
|
|
90
|
+
/* stop the server: stop the keepalive pings, close all subscriptions with close code
|
|
91
|
+
1001, stop the co-hosted agent relay, unbind the listener, and close the storage */
|
|
92
|
+
async stop() {
|
|
93
|
+
if (this.timer !== null)
|
|
94
|
+
clearInterval(this.timer);
|
|
95
|
+
this.timer = null;
|
|
96
|
+
for (const subs of this.subs.values())
|
|
97
|
+
for (const sub of subs)
|
|
98
|
+
sub.ws.close(1001, "server shutting down");
|
|
99
|
+
this.subs.clear();
|
|
100
|
+
this.relay?.stop();
|
|
101
|
+
this.wss.close();
|
|
102
|
+
await this.server.stop({ timeout: 1000 });
|
|
103
|
+
await this.options.store.close();
|
|
104
|
+
this.log.write("info", "task store: stopped");
|
|
105
|
+
}
|
|
106
|
+
/* read the path parameters and a single-valued query parameter */
|
|
107
|
+
params(request) {
|
|
108
|
+
return request.params;
|
|
109
|
+
}
|
|
110
|
+
query(request, name) {
|
|
111
|
+
const value = request.query[name];
|
|
112
|
+
if (Array.isArray(value))
|
|
113
|
+
return value[value.length - 1];
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
/* the lifecycle model, task id scheme, and sequence number
|
|
117
|
+
high-water mark as exposed by the project endpoints */
|
|
118
|
+
projectView(project) {
|
|
119
|
+
return {
|
|
120
|
+
id: project.id,
|
|
121
|
+
lifecycle: {
|
|
122
|
+
name: project.lifecycle.name,
|
|
123
|
+
states: project.lifecycle.states,
|
|
124
|
+
initial: project.lifecycle.initial,
|
|
125
|
+
finished: project.lifecycle.finished,
|
|
126
|
+
transitions: project.lifecycle.transitions
|
|
127
|
+
},
|
|
128
|
+
idscheme: project.idscheme,
|
|
129
|
+
seqmark: project.seqmark
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/* strip the "created" flag of a core result and map it onto the status code */
|
|
133
|
+
created(h, result) {
|
|
134
|
+
const { created, ...body } = result;
|
|
135
|
+
return h.response(body).code(created ? 201 : 200);
|
|
136
|
+
}
|
|
137
|
+
/* ==== events ==== */
|
|
138
|
+
/* ping all subscribers, terminating those which have not answered the previous
|
|
139
|
+
ping (e.g. on a half-open connection after a network partition or a host crash) */
|
|
140
|
+
keepalive() {
|
|
141
|
+
for (const [prjId, subs] of this.subs)
|
|
142
|
+
for (const sub of subs) {
|
|
143
|
+
if (this.unanswered.has(sub.ws)) {
|
|
144
|
+
this.log.write("debug", `task store: terminating unresponsive subscriber of project "${prjId}"`);
|
|
145
|
+
sub.ws.terminate();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
this.unanswered.add(sub.ws);
|
|
149
|
+
sub.ws.ping();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/* deliver an event frame to the subscribers of a project, each
|
|
153
|
+
restricted to the task ids it subscribed to (except for a
|
|
154
|
+
lifecycle model or task id scheme change, which concerns all task plans),
|
|
155
|
+
terminating the subscribers which cannot keep up (as they would miss
|
|
156
|
+
events, and are notified of a change on their reconnection anyway) */
|
|
157
|
+
emit(prjId, frame) {
|
|
158
|
+
const subs = this.subs.get(prjId);
|
|
159
|
+
if (subs === undefined)
|
|
160
|
+
return;
|
|
161
|
+
for (const sub of subs) {
|
|
162
|
+
if (sub.ws.readyState !== sub.ws.OPEN)
|
|
163
|
+
continue;
|
|
164
|
+
if (sub.ws.bufferedAmount > BUFFER_MAX) {
|
|
165
|
+
this.log.write("debug", `task store: terminating congested subscriber of project "${prjId}"`);
|
|
166
|
+
sub.ws.terminate();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const out = {};
|
|
170
|
+
const keep = (id) => sub.tasks === null || sub.tasks.has(id);
|
|
171
|
+
if (frame.added !== undefined) {
|
|
172
|
+
const added = Object.fromEntries(Object.entries(frame.added).filter(([id]) => keep(id)));
|
|
173
|
+
if (Object.keys(added).length > 0)
|
|
174
|
+
out.added = added;
|
|
175
|
+
}
|
|
176
|
+
if (frame.updated !== undefined) {
|
|
177
|
+
const updated = Object.fromEntries(Object.entries(frame.updated).filter(([id]) => keep(id)));
|
|
178
|
+
if (Object.keys(updated).length > 0)
|
|
179
|
+
out.updated = updated;
|
|
180
|
+
}
|
|
181
|
+
if (frame.deleted !== undefined) {
|
|
182
|
+
const deleted = frame.deleted.filter((id) => keep(id));
|
|
183
|
+
if (deleted.length > 0)
|
|
184
|
+
out.deleted = deleted;
|
|
185
|
+
}
|
|
186
|
+
if (frame.lifecycle !== undefined)
|
|
187
|
+
out.lifecycle = frame.lifecycle;
|
|
188
|
+
if (frame.idscheme !== undefined)
|
|
189
|
+
out.idscheme = frame.idscheme;
|
|
190
|
+
if (Object.keys(out).length > 0)
|
|
191
|
+
sub.ws.send(JSON.stringify(out));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/* handle a WebSocket handshake on the event endpoint (or, after its
|
|
195
|
+
authentication, on the event endpoint of the co-hosted agent relay) */
|
|
196
|
+
async upgrade(req, socket, head) {
|
|
197
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
198
|
+
const path = url.pathname;
|
|
199
|
+
if (this.relay !== null && AgentRelayBinding.handles(path)) {
|
|
200
|
+
if (!this.guard.authorized(req.headers.authorization, url.searchParams.get("token") ?? undefined))
|
|
201
|
+
this.guard.rejectUpgrade(socket, 401, "missing or invalid bearer token", path);
|
|
202
|
+
else
|
|
203
|
+
this.relay.upgrade(req, socket, head, path, (status, detail) => {
|
|
204
|
+
this.guard.rejectUpgrade(socket, status, detail, path);
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const m = /^\/projects\/([^/]+)\/events$/.exec(path);
|
|
209
|
+
if (m === null) {
|
|
210
|
+
this.guard.rejectUpgrade(socket, 404, `no WebSocket endpoint "${path}"`, path);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!this.guard.authorized(req.headers.authorization, url.searchParams.get("token") ?? undefined)) {
|
|
214
|
+
this.guard.rejectUpgrade(socket, 401, "missing or invalid bearer token", path);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const prjId = m[1];
|
|
218
|
+
if (!TaskFormat.ID_RE.test(prjId)) {
|
|
219
|
+
this.guard.rejectUpgrade(socket, 422, `invalid project id "${prjId}" (expected: [A-Za-z0-9_-]+)`, path);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!await this.core.projectExists(prjId)) {
|
|
223
|
+
this.guard.rejectUpgrade(socket, 404, `no project "${prjId}" registered`, path);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let tasks = null;
|
|
227
|
+
const list = url.searchParams.get("tasks");
|
|
228
|
+
if (list !== null) {
|
|
229
|
+
tasks = new Set();
|
|
230
|
+
for (const id of list.split(",").map((token) => token.trim()).filter((token) => token !== "")) {
|
|
231
|
+
if (!TaskFormat.TASK_ID_RE.test(id)) {
|
|
232
|
+
this.guard.rejectUpgrade(socket, 422, `invalid task id "${id}" (expected: [A-Za-z0-9#][A-Za-z0-9#_-]*)`, path);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
tasks.add(id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/* reject a subscriber beyond the maximum numbers */
|
|
239
|
+
let total = 0;
|
|
240
|
+
for (const subs of this.subs.values())
|
|
241
|
+
total += subs.size;
|
|
242
|
+
if ((this.subs.get(prjId)?.size ?? 0) >= SUBS_MAX) {
|
|
243
|
+
this.guard.rejectUpgrade(socket, 429, `too many subscribers of project "${prjId}" (maximum: ${SUBS_MAX})`, path);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (total >= SUBS_TOTAL_MAX) {
|
|
247
|
+
this.guard.rejectUpgrade(socket, 429, `too many subscribers in total (maximum: ${SUBS_TOTAL_MAX})`, path);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
251
|
+
const sub = { ws, tasks };
|
|
252
|
+
let subs = this.subs.get(prjId);
|
|
253
|
+
if (subs === undefined) {
|
|
254
|
+
subs = new Set();
|
|
255
|
+
this.subs.set(prjId, subs);
|
|
256
|
+
}
|
|
257
|
+
subs.add(sub);
|
|
258
|
+
this.log.write("debug", `task store: subscribed to events of project "${prjId}"` +
|
|
259
|
+
(tasks !== null ? ` (tasks: ${[...tasks].join(", ")})` : ""));
|
|
260
|
+
ws.on("pong", () => {
|
|
261
|
+
this.unanswered.delete(ws);
|
|
262
|
+
});
|
|
263
|
+
ws.on("close", () => {
|
|
264
|
+
const subs = this.subs.get(prjId);
|
|
265
|
+
if (subs !== undefined) {
|
|
266
|
+
subs.delete(sub);
|
|
267
|
+
if (subs.size === 0)
|
|
268
|
+
this.subs.delete(prjId);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
ws.on("error", (err) => {
|
|
272
|
+
this.log.write("debug", `task store: WebSocket error on events of project "${prjId}": ${err.message}`);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/* ==== routes ==== */
|
|
277
|
+
/* install the routes onto the core operations (and of the co-hosted agent relay) */
|
|
278
|
+
installRoutes() {
|
|
279
|
+
this.installProjectRoutes();
|
|
280
|
+
this.installTaskRoutes();
|
|
281
|
+
this.installPartRoutes();
|
|
282
|
+
this.installAttachmentRoutes();
|
|
283
|
+
this.relay?.install(this.server);
|
|
284
|
+
}
|
|
285
|
+
/* install the project endpoints (and the plain HTTP fallback of the event endpoint) */
|
|
286
|
+
installProjectRoutes() {
|
|
287
|
+
const core = this.core;
|
|
288
|
+
const p = (request) => this.params(request);
|
|
289
|
+
/* the project endpoints */
|
|
290
|
+
this.server.route({
|
|
291
|
+
method: "GET",
|
|
292
|
+
path: "/projects",
|
|
293
|
+
handler: async () => ({ projects: await core.projectList() })
|
|
294
|
+
});
|
|
295
|
+
this.server.route({
|
|
296
|
+
method: "GET",
|
|
297
|
+
path: P,
|
|
298
|
+
handler: async (request) => this.projectView(await core.projectGet(p(request).prjId))
|
|
299
|
+
});
|
|
300
|
+
this.server.route({
|
|
301
|
+
method: "PUT",
|
|
302
|
+
path: P,
|
|
303
|
+
options: json,
|
|
304
|
+
handler: async (request, h) => {
|
|
305
|
+
const payload = request.payload;
|
|
306
|
+
if (payload !== null && payload !== undefined && !Core.isObject(payload))
|
|
307
|
+
throw Core.problem(400, "request body has to be an object");
|
|
308
|
+
/* "If-None-Match: *" registers the project only if not yet registered */
|
|
309
|
+
const createOnly = request.headers["if-none-match"] === "*";
|
|
310
|
+
const result = await core.projectSet(p(request).prjId, Core.isObject(payload) ?
|
|
311
|
+
{ lifecycle: payload.lifecycle, idscheme: payload.idscheme } : {}, createOnly);
|
|
312
|
+
return h.response(this.projectView(result.project)).code(result.created ? 201 : 200);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
this.server.route({
|
|
316
|
+
method: "DELETE",
|
|
317
|
+
path: P,
|
|
318
|
+
handler: async (request, h) => {
|
|
319
|
+
const prjId = p(request).prjId;
|
|
320
|
+
await core.projectDelete(prjId);
|
|
321
|
+
const subs = this.subs.get(prjId);
|
|
322
|
+
if (subs !== undefined) {
|
|
323
|
+
for (const sub of subs)
|
|
324
|
+
sub.ws.close(1001, "project unregistered");
|
|
325
|
+
this.subs.delete(prjId);
|
|
326
|
+
}
|
|
327
|
+
return h.response().code(204);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
/* a plain HTTP request on the event endpoint (the WebSocket
|
|
331
|
+
handshakes are intercepted on the listener's "upgrade" event) */
|
|
332
|
+
this.server.route({
|
|
333
|
+
method: "GET",
|
|
334
|
+
path: `${P}/events`,
|
|
335
|
+
handler: async (request) => {
|
|
336
|
+
await core.projectGet(p(request).prjId);
|
|
337
|
+
throw Core.problem(426, "the event endpoint requires a WebSocket handshake");
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/* install the task plan endpoints */
|
|
342
|
+
installTaskRoutes() {
|
|
343
|
+
const core = this.core;
|
|
344
|
+
const p = (request) => this.params(request);
|
|
345
|
+
/* the task plan endpoints */
|
|
346
|
+
this.server.route({
|
|
347
|
+
method: "GET",
|
|
348
|
+
path: `${P}/tasks`,
|
|
349
|
+
handler: async (request) => ({
|
|
350
|
+
tasks: await core.taskList(p(request).prjId, this.query(request, "include"), this.query(request, "exclude"), this.query(request, "fields"))
|
|
351
|
+
})
|
|
352
|
+
});
|
|
353
|
+
this.server.route({
|
|
354
|
+
method: "DELETE",
|
|
355
|
+
path: `${P}/tasks`,
|
|
356
|
+
handler: async (request) => ({ purged: await core.taskPurge(p(request).prjId, this.query(request, "age")) })
|
|
357
|
+
});
|
|
358
|
+
this.server.route({
|
|
359
|
+
method: "POST",
|
|
360
|
+
path: `${P}/newid`,
|
|
361
|
+
options: json,
|
|
362
|
+
handler: async (request) => {
|
|
363
|
+
const payload = request.payload;
|
|
364
|
+
if (payload !== null && payload !== undefined && !Core.isObject(payload))
|
|
365
|
+
throw Core.problem(400, "request body has to be an object");
|
|
366
|
+
return core.taskNewId(p(request).prjId, Core.isObject(payload) ? payload : {});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
this.server.route({
|
|
370
|
+
method: "GET",
|
|
371
|
+
path: T,
|
|
372
|
+
handler: async (request, h) => {
|
|
373
|
+
const plan = await core.taskLoad(p(request).prjId, p(request).taskId);
|
|
374
|
+
return h.response(plan).etag(Core.taskTag(plan));
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
this.server.route({
|
|
378
|
+
method: "PUT",
|
|
379
|
+
path: T,
|
|
380
|
+
options: json,
|
|
381
|
+
handler: async (request, h) => {
|
|
382
|
+
/* "If-Match: <tag>" saves only if the plan is still the one with this entity tag
|
|
383
|
+
(ignoring the content encoding suffix hapi appends to the tag of a compressed response),
|
|
384
|
+
and "If-None-Match: *" saves only if the plan does not exist yet */
|
|
385
|
+
const tag = request.headers["if-match"];
|
|
386
|
+
return this.created(h, await core.taskSave(p(request).prjId, p(request).taskId, request.payload, typeof tag === "string" ? tag.replace(/^(?:W\/)?"(.*?)(?:-(?:gzip|deflate))?"$/, "$1") : undefined, request.headers["if-none-match"] === "*"));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
this.server.route({
|
|
390
|
+
method: "PATCH",
|
|
391
|
+
path: T,
|
|
392
|
+
options: json,
|
|
393
|
+
handler: async (request) => core.taskPatch(p(request).prjId, p(request).taskId, request.payload)
|
|
394
|
+
});
|
|
395
|
+
this.server.route({
|
|
396
|
+
method: "DELETE",
|
|
397
|
+
path: T,
|
|
398
|
+
handler: async (request, h) => {
|
|
399
|
+
await core.taskDelete(p(request).prjId, p(request).taskId);
|
|
400
|
+
return h.response().code(204);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/* install the header and body part endpoints */
|
|
405
|
+
installPartRoutes() {
|
|
406
|
+
const core = this.core;
|
|
407
|
+
const p = (request) => this.params(request);
|
|
408
|
+
/* the header part endpoints */
|
|
409
|
+
this.server.route({
|
|
410
|
+
method: "GET",
|
|
411
|
+
path: `${T}/header`,
|
|
412
|
+
handler: async (request) => core.headerGet(p(request).prjId, p(request).taskId)
|
|
413
|
+
});
|
|
414
|
+
this.server.route({
|
|
415
|
+
method: "PUT",
|
|
416
|
+
path: `${T}/header`,
|
|
417
|
+
options: json,
|
|
418
|
+
handler: async (request) => {
|
|
419
|
+
const { created, ...body } = await core.headerSet(p(request).prjId, p(request).taskId, request.payload);
|
|
420
|
+
return body;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
this.server.route({
|
|
424
|
+
method: "GET",
|
|
425
|
+
path: `${T}/header/content`,
|
|
426
|
+
handler: async (request, h) => h.response(await core.headerContent(p(request).prjId, p(request).taskId)).type("text/plain; charset=utf-8")
|
|
427
|
+
});
|
|
428
|
+
this.server.route({
|
|
429
|
+
method: "GET",
|
|
430
|
+
path: `${T}/header/key/{key}`,
|
|
431
|
+
handler: async (request) => ({ value: await core.headerKeyGet(p(request).prjId, p(request).taskId, p(request).key) })
|
|
432
|
+
});
|
|
433
|
+
this.server.route({
|
|
434
|
+
method: "PUT",
|
|
435
|
+
path: `${T}/header/key/{key}`,
|
|
436
|
+
options: json,
|
|
437
|
+
handler: async (request, h) => this.created(h, await core.headerKeySet(p(request).prjId, p(request).taskId, p(request).key, request.payload))
|
|
438
|
+
});
|
|
439
|
+
this.server.route({
|
|
440
|
+
method: "DELETE",
|
|
441
|
+
path: `${T}/header/key/{key}`,
|
|
442
|
+
handler: async (request, h) => {
|
|
443
|
+
await core.headerKeyDelete(p(request).prjId, p(request).taskId, p(request).key);
|
|
444
|
+
return h.response().code(204);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
/* the body part endpoints */
|
|
448
|
+
this.server.route({
|
|
449
|
+
method: "GET",
|
|
450
|
+
path: `${T}/body`,
|
|
451
|
+
handler: async (request) => ({ body: await core.bodyGet(p(request).prjId, p(request).taskId) })
|
|
452
|
+
});
|
|
453
|
+
this.server.route({
|
|
454
|
+
method: "PUT",
|
|
455
|
+
path: `${T}/body`,
|
|
456
|
+
options: json,
|
|
457
|
+
handler: async (request, h) => {
|
|
458
|
+
await core.bodySet(p(request).prjId, p(request).taskId, request.payload);
|
|
459
|
+
return h.response().code(204);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/* install the attachment part endpoints */
|
|
464
|
+
installAttachmentRoutes() {
|
|
465
|
+
const core = this.core;
|
|
466
|
+
const p = (request) => this.params(request);
|
|
467
|
+
/* the attachment part endpoints */
|
|
468
|
+
this.server.route({
|
|
469
|
+
method: "GET",
|
|
470
|
+
path: `${T}/attachment`,
|
|
471
|
+
handler: async (request) => {
|
|
472
|
+
const type = this.query(request, "type");
|
|
473
|
+
if (type === undefined)
|
|
474
|
+
return core.attachmentList(p(request).prjId, p(request).taskId);
|
|
475
|
+
return { found: await core.attachmentFind(p(request).prjId, p(request).taskId, type) };
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
this.server.route({
|
|
479
|
+
method: "POST",
|
|
480
|
+
path: `${T}/attachment`,
|
|
481
|
+
options: json,
|
|
482
|
+
handler: async (request, h) => {
|
|
483
|
+
const append = this.query(request, "append") === "true";
|
|
484
|
+
const { index, created } = await core.attachmentAdd(p(request).prjId, p(request).taskId, request.payload, append);
|
|
485
|
+
return h.response({ index }).code(created ? 201 : 200).header("Location", `${request.path}/${index}`);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
this.server.route({
|
|
489
|
+
method: "GET",
|
|
490
|
+
path: `${T}/attachment/{index}`,
|
|
491
|
+
handler: async (request) => core.attachmentGet(p(request).prjId, p(request).taskId, p(request).index)
|
|
492
|
+
});
|
|
493
|
+
this.server.route({
|
|
494
|
+
method: "PUT",
|
|
495
|
+
path: `${T}/attachment/{index}`,
|
|
496
|
+
options: json,
|
|
497
|
+
handler: async (request, h) => {
|
|
498
|
+
await core.attachmentSet(p(request).prjId, p(request).taskId, p(request).index, request.payload);
|
|
499
|
+
return h.response().code(204);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
this.server.route({
|
|
503
|
+
method: "DELETE",
|
|
504
|
+
path: `${T}/attachment/{index}`,
|
|
505
|
+
handler: async (request, h) => {
|
|
506
|
+
await core.attachmentDelete(p(request).prjId, p(request).taskId, p(request).index);
|
|
507
|
+
return h.response().code(204);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
this.server.route({
|
|
511
|
+
method: "GET",
|
|
512
|
+
path: `${T}/attachment/{index}/content`,
|
|
513
|
+
handler: async (request, h) => {
|
|
514
|
+
const { type, content } = await core.attachmentContent(p(request).prjId, p(request).taskId, p(request).index);
|
|
515
|
+
return h.response(content).type(type);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
this.server.route({
|
|
519
|
+
method: "GET",
|
|
520
|
+
path: `${T}/attachment/{index}/key/{key}`,
|
|
521
|
+
handler: async (request) => ({ value: await core.attachmentKeyGet(p(request).prjId, p(request).taskId, p(request).index, p(request).key) })
|
|
522
|
+
});
|
|
523
|
+
this.server.route({
|
|
524
|
+
method: "PUT",
|
|
525
|
+
path: `${T}/attachment/{index}/key/{key}`,
|
|
526
|
+
options: json,
|
|
527
|
+
handler: async (request, h) => this.created(h, await core.attachmentKeySet(p(request).prjId, p(request).taskId, p(request).index, p(request).key, request.payload))
|
|
528
|
+
});
|
|
529
|
+
this.server.route({
|
|
530
|
+
method: "DELETE",
|
|
531
|
+
path: `${T}/attachment/{index}/key/{key}`,
|
|
532
|
+
handler: async (request, h) => {
|
|
533
|
+
await core.attachmentKeyDelete(p(request).prjId, p(request).taskId, p(request).index, p(request).key);
|
|
534
|
+
return h.response().code(204);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|