@monster-orange/den 0.0.0 → 0.1.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/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +99 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app under test, as the journeys pillar needs to know it.
|
|
3
|
+
*
|
|
4
|
+
* The engine knows nothing about any one product's API. Everything
|
|
5
|
+
* app-specific (which endpoints create resources and how to delete them,
|
|
6
|
+
* how a browser session yields an API token, which web-origin routes proxy
|
|
7
|
+
* the API) is declared by the repo that hosts the journey specs, under
|
|
8
|
+
* `journeys.app` in its manifest:
|
|
9
|
+
*
|
|
10
|
+
* journeys:
|
|
11
|
+
* testDir: journeys
|
|
12
|
+
* testMatch: "**\/*.journey.yml"
|
|
13
|
+
* app:
|
|
14
|
+
* session_token: # how a browser session yields an API token
|
|
15
|
+
* path: /api/auth/token # GET on the web origin with the session cookies
|
|
16
|
+
* token_field: access_token
|
|
17
|
+
* headers: # extra request headers, from response fields
|
|
18
|
+
* X-Org-ID: org_id
|
|
19
|
+
* web_api: # API calls the browser makes via the web origin
|
|
20
|
+
* prefix: /api/
|
|
21
|
+
* ignore: [/api/auth/]
|
|
22
|
+
* rewrites: # web path prefix -> API path prefix
|
|
23
|
+
* - { from: /api/items, to: /api/v1/items }
|
|
24
|
+
* resources: # creations the run ledger tracks and tears down
|
|
25
|
+
* - type: item
|
|
26
|
+
* create: POST /api/v1/items
|
|
27
|
+
* delete: DELETE /api/v1/items/{id}
|
|
28
|
+
* actions: # mutating calls that create nothing to clean up
|
|
29
|
+
* - POST /api/v1/items/*\/archive
|
|
30
|
+
*
|
|
31
|
+
* Patterns use `*` for exactly one path segment (the same syntax as a
|
|
32
|
+
* spec's `http_capabilities`). Every field is optional; an absent block
|
|
33
|
+
* means: no tracked resources, no web-origin API feed, and no session
|
|
34
|
+
* token (a journey that needs API assertions then fails loudly).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import type {
|
|
39
|
+
CleanupRequest,
|
|
40
|
+
HttpMethod,
|
|
41
|
+
LedgerRegistry,
|
|
42
|
+
ObservedCall,
|
|
43
|
+
ResourceDescriptor,
|
|
44
|
+
} from "./tools/ledger.js";
|
|
45
|
+
|
|
46
|
+
const HTTP_METHODS = [
|
|
47
|
+
"GET",
|
|
48
|
+
"HEAD",
|
|
49
|
+
"OPTIONS",
|
|
50
|
+
"POST",
|
|
51
|
+
"PUT",
|
|
52
|
+
"PATCH",
|
|
53
|
+
"DELETE",
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
const absolutePath = z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(/^\//, { message: "must be an absolute path (start with /)" });
|
|
59
|
+
|
|
60
|
+
/** "METHOD /path" with an optional method (defaulted per field). */
|
|
61
|
+
const routeString = z.string().regex(/^(?:[A-Z]+\s+)?\/\S*$/, {
|
|
62
|
+
message: 'must look like "METHOD /path" or "/path"',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sessionTokenSchema = z.strictObject({
|
|
66
|
+
/** GET path on the web origin that returns a JSON token for the session. */
|
|
67
|
+
path: absolutePath,
|
|
68
|
+
/** Response field holding the bearer token (default "access_token"). */
|
|
69
|
+
token_field: z.string().min(1).optional(),
|
|
70
|
+
/**
|
|
71
|
+
* Extra headers every API call carries: header name -> response field
|
|
72
|
+
* (for example a workspace or org id the API scopes requests by).
|
|
73
|
+
*/
|
|
74
|
+
headers: z.record(z.string().min(1), z.string().min(1)).optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const webApiSchema = z.strictObject({
|
|
78
|
+
/** Web-origin path prefix whose mutations feed the run ledger. */
|
|
79
|
+
prefix: absolutePath,
|
|
80
|
+
/** Web-origin prefixes never recorded (login and session routes). */
|
|
81
|
+
ignore: z.array(absolutePath).optional(),
|
|
82
|
+
/** Web path prefix -> API path prefix, so both feeds bind the same id. */
|
|
83
|
+
rewrites: z
|
|
84
|
+
.array(z.strictObject({ from: absolutePath, to: absolutePath }))
|
|
85
|
+
.optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const resourceSchema = z.strictObject({
|
|
89
|
+
/** Binds `{<type>_id}` placeholders in assertion probes. */
|
|
90
|
+
type: z.string().regex(/^[a-z][a-z0-9_]*$/, {
|
|
91
|
+
message: "must be lower-case letters, digits and _",
|
|
92
|
+
}),
|
|
93
|
+
/** Creating call, "POST /api/v1/items" (method defaults to POST). */
|
|
94
|
+
create: routeString,
|
|
95
|
+
/** Cleanup call with an {id} placeholder (method defaults to DELETE). */
|
|
96
|
+
delete: routeString.refine((v) => v.includes("{id}"), {
|
|
97
|
+
message: "must contain {id}",
|
|
98
|
+
}),
|
|
99
|
+
/** Response-body field holding the new id (default "id"). */
|
|
100
|
+
id_field: z.string().min(1).optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const journeysAppSchema = z.strictObject({
|
|
104
|
+
session_token: sessionTokenSchema.optional(),
|
|
105
|
+
web_api: webApiSchema.optional(),
|
|
106
|
+
resources: z.array(resourceSchema).optional(),
|
|
107
|
+
/** "METHOD /path" patterns (method defaults to POST) never recorded. */
|
|
108
|
+
actions: z.array(routeString).optional(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export type JourneysAppConfig = z.infer<typeof journeysAppSchema>;
|
|
112
|
+
export type JourneysSessionTokenConfig = z.infer<typeof sessionTokenSchema>;
|
|
113
|
+
export type JourneysWebApiConfig = z.infer<typeof webApiSchema>;
|
|
114
|
+
|
|
115
|
+
/** Thrown when a manifest's `journeys.app` block is invalid. */
|
|
116
|
+
export class JourneysAppConfigError extends Error {
|
|
117
|
+
constructor(source: string, detail: string) {
|
|
118
|
+
super(`Invalid journeys.app in ${source}: ${detail}`);
|
|
119
|
+
this.name = "JourneysAppConfigError";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Parse a raw `journeys.app` block; absent → {}. Throws on any error. */
|
|
124
|
+
export function parseJourneysAppConfig(
|
|
125
|
+
raw: unknown,
|
|
126
|
+
source: string,
|
|
127
|
+
): JourneysAppConfig {
|
|
128
|
+
if (raw === undefined || raw === null) return {};
|
|
129
|
+
const result = journeysAppSchema.safeParse(raw);
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
throw new JourneysAppConfigError(
|
|
132
|
+
source,
|
|
133
|
+
result.error.issues
|
|
134
|
+
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
|
|
135
|
+
.join("; "),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return result.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Ledger registry from config ──────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function escapeRegExp(value: string): string {
|
|
144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `*` → exactly one path segment, the rest literal, anchored. */
|
|
148
|
+
export function segmentPatternToRegExp(pattern: string): RegExp {
|
|
149
|
+
const source = pattern.split("*").map(escapeRegExp).join("[^/]+");
|
|
150
|
+
return new RegExp(`^${source}$`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function splitRoute(
|
|
154
|
+
route: string,
|
|
155
|
+
defaultMethod: HttpMethod,
|
|
156
|
+
): { method: HttpMethod; path: string } {
|
|
157
|
+
const m = /^([A-Z]+)\s+(\S+)$/.exec(route.trim());
|
|
158
|
+
if (!m) return { method: defaultMethod, path: route.trim() };
|
|
159
|
+
if (!(HTTP_METHODS as readonly string[]).includes(m[1])) {
|
|
160
|
+
throw new Error(`Unknown HTTP method "${m[1]}" in "${route}"`);
|
|
161
|
+
}
|
|
162
|
+
return { method: m[1] as HttpMethod, path: m[2] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function idExtractor(
|
|
166
|
+
field: string,
|
|
167
|
+
): (call: ObservedCall) => string | undefined {
|
|
168
|
+
return (call) => {
|
|
169
|
+
if (typeof call.body !== "object" || call.body === null) return undefined;
|
|
170
|
+
const id = (call.body as Record<string, unknown>)[field];
|
|
171
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build the run ledger's registry from `journeys.app`: one descriptor per
|
|
177
|
+
* declared resource and the declared action patterns. No resources → an
|
|
178
|
+
* empty registry, so every successful mutation is reported as unmanaged
|
|
179
|
+
* (never guessed-deleted).
|
|
180
|
+
*/
|
|
181
|
+
export function ledgerRegistryFromApp(app: JourneysAppConfig): LedgerRegistry {
|
|
182
|
+
const descriptors: ResourceDescriptor[] = (app.resources ?? []).map((r) => {
|
|
183
|
+
const create = splitRoute(r.create, "POST");
|
|
184
|
+
const del = splitRoute(r.delete, "DELETE");
|
|
185
|
+
return {
|
|
186
|
+
resourceType: r.type,
|
|
187
|
+
creation: {
|
|
188
|
+
method: create.method,
|
|
189
|
+
pathPattern: segmentPatternToRegExp(create.path),
|
|
190
|
+
},
|
|
191
|
+
extractId: idExtractor(r.id_field ?? "id"),
|
|
192
|
+
cleanup: (id: string): CleanupRequest => ({
|
|
193
|
+
method: del.method,
|
|
194
|
+
path: del.path.split("{id}").join(encodeURIComponent(id)),
|
|
195
|
+
}),
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const actionPatterns = (app.actions ?? []).map((a) =>
|
|
199
|
+
segmentPatternToRegExp(splitRoute(a, "POST").path),
|
|
200
|
+
);
|
|
201
|
+
return { descriptors, actionPatterns };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Web-origin path rewrites ─────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Map a web-origin path onto the API vocabulary via `web_api.rewrites`.
|
|
208
|
+
* A rewrite applies when `from` equals the path or is followed by "/"
|
|
209
|
+
* (segment-aware: `/api/items` never rewrites `/api/items-old`). The
|
|
210
|
+
* longest matching `from` wins; no match returns the path unchanged.
|
|
211
|
+
*/
|
|
212
|
+
export function rewriteWebApiPath(
|
|
213
|
+
pathname: string,
|
|
214
|
+
webApi: JourneysWebApiConfig | undefined,
|
|
215
|
+
): string {
|
|
216
|
+
let best: { from: string; to: string } | undefined;
|
|
217
|
+
for (const rule of webApi?.rewrites ?? []) {
|
|
218
|
+
const from = rule.from.replace(/\/+$/, "");
|
|
219
|
+
const matches = pathname === from || pathname.startsWith(`${from}/`);
|
|
220
|
+
if (matches && (best === undefined || from.length > best.from.length)) {
|
|
221
|
+
best = { from, to: rule.to.replace(/\/+$/, "") };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return best === undefined
|
|
225
|
+
? pathname
|
|
226
|
+
: `${best.to}${pathname.slice(best.from.length)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** {@link rewriteWebApiPath} over an absolute URL (origin/query kept). */
|
|
230
|
+
export function rewriteWebApiUrl(
|
|
231
|
+
rawUrl: string,
|
|
232
|
+
webApi: JourneysWebApiConfig | undefined,
|
|
233
|
+
): string {
|
|
234
|
+
const url = new URL(rawUrl);
|
|
235
|
+
url.pathname = rewriteWebApiPath(url.pathname, webApi);
|
|
236
|
+
return url.toString();
|
|
237
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journey spec discovery (journeys pillar).
|
|
3
|
+
*
|
|
4
|
+
* Manifest-driven, mirroring how synthetics resolve their spec home: each
|
|
5
|
+
* registered repo may declare a top-level `journeys: {testDir, testMatch}`
|
|
6
|
+
* block in its .den.yml. Discovery iterates discoverRepos(), resolves
|
|
7
|
+
* testDir against the registered local checkout, globs testMatch, and
|
|
8
|
+
* validates every matching file through the journey spec loader.
|
|
9
|
+
*
|
|
10
|
+
* Failure posture is LOUD by design:
|
|
11
|
+
* - a declared testDir that does not exist on disk throws (misconfigured
|
|
12
|
+
* manifest or unregistered checkout — never silently zero specs);
|
|
13
|
+
* - an invalid spec file throws JourneySpecError carrying the file path —
|
|
14
|
+
* invalid specs are never skipped.
|
|
15
|
+
*
|
|
16
|
+
* Deployed-SHA clone parity for non-local environments is deferred to the
|
|
17
|
+
* jrn-manifest-ci packet; v1 always reads from the registered checkout.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import { discoverRepos } from "../smoke/discover.js";
|
|
23
|
+
import { loadJourneySpec } from "./spec/loader.js";
|
|
24
|
+
import {
|
|
25
|
+
parseJourneysAppConfig,
|
|
26
|
+
type JourneysAppConfig,
|
|
27
|
+
} from "./app-config.js";
|
|
28
|
+
import type { JourneySpec } from "./spec/schema.js";
|
|
29
|
+
import type { DiscoveredRepo } from "../../shared/types/index.js";
|
|
30
|
+
|
|
31
|
+
import { product } from "../../shared/product.js";
|
|
32
|
+
// ── Result shape ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** A validated journey spec with its source repo/path metadata. */
|
|
35
|
+
export interface DiscoveredJourneySpec {
|
|
36
|
+
/** Manifest name of the repo the spec came from. */
|
|
37
|
+
repoName: string;
|
|
38
|
+
/** Absolute path of the registered repo checkout. */
|
|
39
|
+
repoPath: string;
|
|
40
|
+
/** Absolute path of the *.journey.yml file. */
|
|
41
|
+
specPath: string;
|
|
42
|
+
/** The loader-validated spec. */
|
|
43
|
+
spec: JourneySpec;
|
|
44
|
+
/** The app under test, from the repo manifest's `journeys.app` ({} if absent). */
|
|
45
|
+
app: JourneysAppConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Glob matching (dependency-free) ──────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert a testMatch glob (e.g. "**\/*.journey.yml") to a RegExp over
|
|
52
|
+
* POSIX-style relative paths. Supports `**` (any depth, including zero),
|
|
53
|
+
* `*` (within one segment), and `?` (single char within a segment).
|
|
54
|
+
*/
|
|
55
|
+
export function globToRegExp(pattern: string): RegExp {
|
|
56
|
+
let out = "";
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < pattern.length) {
|
|
59
|
+
const ch = pattern[i];
|
|
60
|
+
if (ch === "*") {
|
|
61
|
+
if (pattern[i + 1] === "*") {
|
|
62
|
+
// `**/` matches zero or more whole segments; a trailing `**` matches
|
|
63
|
+
// anything (including slashes).
|
|
64
|
+
if (pattern[i + 2] === "/") {
|
|
65
|
+
out += "(?:[^/]+/)*";
|
|
66
|
+
i += 3;
|
|
67
|
+
} else {
|
|
68
|
+
out += ".*";
|
|
69
|
+
i += 2;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
out += "[^/]*";
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
} else if (ch === "?") {
|
|
76
|
+
out += "[^/]";
|
|
77
|
+
i += 1;
|
|
78
|
+
} else {
|
|
79
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return new RegExp(`^${out}$`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Recursively collect all regular files under `dir` (absolute paths). */
|
|
87
|
+
function walkFiles(dir: string): string[] {
|
|
88
|
+
const files: string[] = [];
|
|
89
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
90
|
+
const abs = path.join(dir, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
files.push(...walkFiles(abs));
|
|
93
|
+
} else if (entry.isFile()) {
|
|
94
|
+
files.push(abs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Discover and validate journey specs across registered repos.
|
|
104
|
+
*
|
|
105
|
+
* @param repos Repo list to scan; defaults to discoverRepos() (workspace or
|
|
106
|
+
* registry). Injectable for tests.
|
|
107
|
+
* @throws Error when a repo declares journeys but its testDir is missing.
|
|
108
|
+
* @throws JourneySpecError when any matched spec file fails validation.
|
|
109
|
+
*/
|
|
110
|
+
export function discoverJourneySpecs(
|
|
111
|
+
repos: DiscoveredRepo[] = discoverRepos(),
|
|
112
|
+
): DiscoveredJourneySpec[] {
|
|
113
|
+
const results: DiscoveredJourneySpec[] = [];
|
|
114
|
+
|
|
115
|
+
const sortedRepos = [...repos].sort((a, b) =>
|
|
116
|
+
a.repoName.localeCompare(b.repoName),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
for (const repo of sortedRepos) {
|
|
120
|
+
const journeys = repo.manifest.journeys;
|
|
121
|
+
if (!journeys) continue;
|
|
122
|
+
|
|
123
|
+
const testDir = path.resolve(repo.repoPath, journeys.testDir);
|
|
124
|
+
if (!fs.existsSync(testDir)) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Journeys testDir "${journeys.testDir}" declared in ${repo.repoName}'s ` +
|
|
127
|
+
`manifest does not exist at ${testDir}. ` +
|
|
128
|
+
`Check the journeys: block in its ${product().manifestYml} or re-register the repo.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An invalid app block throws loudly, like an invalid spec.
|
|
133
|
+
const app = parseJourneysAppConfig(
|
|
134
|
+
journeys.app,
|
|
135
|
+
`${repo.repoName}'s manifest`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const matcher = globToRegExp(journeys.testMatch);
|
|
139
|
+
const specPaths = walkFiles(testDir)
|
|
140
|
+
.filter((abs) =>
|
|
141
|
+
matcher.test(path.relative(testDir, abs).split(path.sep).join("/")),
|
|
142
|
+
)
|
|
143
|
+
.sort((a, b) => a.localeCompare(b));
|
|
144
|
+
|
|
145
|
+
for (const specPath of specPaths) {
|
|
146
|
+
// loadJourneySpec throws JourneySpecError (with the file path) on any
|
|
147
|
+
// invalid spec — discovery deliberately does not catch it.
|
|
148
|
+
const spec = loadJourneySpec(specPath);
|
|
149
|
+
results.push({
|
|
150
|
+
repoName: repo.repoName,
|
|
151
|
+
repoPath: repo.repoPath,
|
|
152
|
+
specPath,
|
|
153
|
+
spec,
|
|
154
|
+
app,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Journeys shell-denial hook (beforeShellExecution).
|
|
4
|
+
*
|
|
5
|
+
* Referenced from the generated `.cursor/hooks.json` in every journey's
|
|
6
|
+
* staged sandbox cwd (see src/pillars/journeys/sandbox.ts, which copies
|
|
7
|
+
* this file into the staged dir so the path resolves from there). Cursor
|
|
8
|
+
* hooks are fail-open by default; the generating side sets
|
|
9
|
+
* `failClosed: true` so a crash of this script still denies. This script
|
|
10
|
+
* itself denies unconditionally: journey agents interact with the
|
|
11
|
+
* app exclusively through the mounted custom tools — shell is never
|
|
12
|
+
* a legitimate capability.
|
|
13
|
+
*
|
|
14
|
+
* Contract (cursor.com/docs/agent/hooks, verified 2026-07-15):
|
|
15
|
+
* stdin: {"command": "<full terminal command>", "cwd": "...", ...}
|
|
16
|
+
* stdout: {"permission": "allow" | "deny" | "ask",
|
|
17
|
+
* "user_message": "...", "agent_message": "..."}
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
function emitDenyAndExit() {
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
permission: "deny",
|
|
26
|
+
user_message: "Shell execution is disabled for journey agents.",
|
|
27
|
+
agent_message:
|
|
28
|
+
"Shell commands are denied in this sandbox. Use the provided " +
|
|
29
|
+
"journey tools (browser_* / http_request) to accomplish your goal.",
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Drain stdin (the hook input is irrelevant — every command is denied),
|
|
36
|
+
// then emit the decision. Any error still ends in a deny.
|
|
37
|
+
try {
|
|
38
|
+
process.stdin.resume();
|
|
39
|
+
process.stdin.on("data", () => {});
|
|
40
|
+
process.stdin.on("end", emitDenyAndExit);
|
|
41
|
+
process.stdin.on("error", emitDenyAndExit);
|
|
42
|
+
} catch {
|
|
43
|
+
emitDenyAndExit();
|
|
44
|
+
}
|