@launchfile/macos-dev 0.12.0 → 0.13.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/dist/bootstrap.d.ts +7 -0
- package/dist/bootstrap.js +28 -4
- package/dist/cli-args.d.ts +24 -0
- package/dist/cli-args.js +51 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +21 -2
- package/dist/env-writer.d.ts +46 -3
- package/dist/env-writer.js +83 -25
- package/dist/health.d.ts +22 -1
- package/dist/health.js +36 -1
- package/dist/https-origin.d.ts +46 -10
- package/dist/https-origin.js +47 -16
- package/dist/lockfile-detect.d.ts +8 -0
- package/dist/lockfile-detect.js +10 -0
- package/dist/prepare-fingerprint.d.ts +23 -0
- package/dist/prepare-fingerprint.js +63 -0
- package/dist/process-manager.d.ts +50 -2
- package/dist/process-manager.js +188 -40
- package/dist/provider.d.ts +67 -1
- package/dist/provider.js +199 -56
- package/dist/redact.js +13 -1
- package/dist/state.d.ts +26 -0
- package/package.json +5 -5
package/dist/bootstrap.d.ts
CHANGED
|
@@ -29,6 +29,13 @@ export declare const BOOTSTRAP_SHELL = "/bin/sh";
|
|
|
29
29
|
*/
|
|
30
30
|
export interface BootstrapResult {
|
|
31
31
|
component: string;
|
|
32
|
+
/**
|
|
33
|
+
* The resolved command in redacted form — never the pre-scrub string
|
|
34
|
+
* (D-18, D-71). `$secrets.*` and `$<resource>.password` resolve to live
|
|
35
|
+
* credentials before the command runs, and this type is a public export:
|
|
36
|
+
* a consumer in another process holds an empty redaction registry and
|
|
37
|
+
* cannot scrub what it is handed.
|
|
38
|
+
*/
|
|
32
39
|
command: string;
|
|
33
40
|
ok: boolean;
|
|
34
41
|
exitCode: number;
|
package/dist/bootstrap.js
CHANGED
|
@@ -32,9 +32,33 @@ export const BOOTSTRAP_SHELL = "/bin/sh";
|
|
|
32
32
|
* CLI tools that detect a TTY will emit color codes that would otherwise
|
|
33
33
|
* break simple patterns like `https?://\S+`.
|
|
34
34
|
*/
|
|
35
|
+
// CSI, OSC, and the single-character escapes a terminal writes into captured
|
|
36
|
+
// output. Every repetition is bounded, and the OSC payload stops at ESC.
|
|
37
|
+
//
|
|
38
|
+
// Unbounded, the OSC branch `\][^\u0007]*` is quadratic: each `ESC ]` in the
|
|
39
|
+
// input rescans the whole remainder looking for a BEL that a hostile log line
|
|
40
|
+
// never supplies, and bootstrap stdout is exactly where a hostile log line
|
|
41
|
+
// arrives (CWE-1333). Through `extractCaptures`, 40 000 `ESC ]` pairs took
|
|
42
|
+
// 366 ms under Bun 1.4.0 on an Apple-silicon Mac; bounded they take 0 ms.
|
|
43
|
+
//
|
|
44
|
+
// Excluding ESC from the payload also closes a swallow: ECMA-48 ends an OSC
|
|
45
|
+
// string at BEL or ST (`ESC \`), and nothing between may contain ESC. The
|
|
46
|
+
// unbounded class ran straight past an ST into the next OSC, so an OSC 8
|
|
47
|
+
// hyperlink lost its link text — and with it the URL a `capture` pattern is
|
|
48
|
+
// looking for.
|
|
49
|
+
//
|
|
50
|
+
// The parameter bound is 64 because 32 is not enough: one SGR that sets a
|
|
51
|
+
// truecolor foreground and background together —
|
|
52
|
+
// `ESC [ 38;2;255;255;255;48;2;240;240;240 m` — carries 33 parameter bytes, and a
|
|
53
|
+
// theme-aware CLI emits it. Past the bound the sequence is not stripped at all
|
|
54
|
+
// and its bytes reach the string `extractCaptures` matches against. ECMA-48
|
|
55
|
+
// permits only a few intermediates; an OSC 8 hyperlink URL is the longest
|
|
56
|
+
// realistic payload and sits far under 1024.
|
|
57
|
+
const ANSI_ESCAPE =
|
|
58
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI match
|
|
59
|
+
/\u001B(?:\[[0-9;?]{0,64}[ -/]{0,8}[@-~]|\][^\u0007\u001B]{0,1024}(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
|
|
35
60
|
function stripAnsi(s) {
|
|
36
|
-
|
|
37
|
-
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, "");
|
|
61
|
+
return s.replace(ANSI_ESCAPE, "");
|
|
38
62
|
}
|
|
39
63
|
/**
|
|
40
64
|
* Apply a set of capture patterns to a command's stdout. If a pattern has
|
|
@@ -209,7 +233,7 @@ export async function launchBootstrap(opts = {}) {
|
|
|
209
233
|
console.error(` \u2717 Bootstrap [${name}]: ${item.error}`);
|
|
210
234
|
results.push({
|
|
211
235
|
component: name,
|
|
212
|
-
command: item.command,
|
|
236
|
+
command: redactSecrets(item.command),
|
|
213
237
|
ok: false,
|
|
214
238
|
exitCode: 1,
|
|
215
239
|
captures: {},
|
|
@@ -260,7 +284,7 @@ export async function launchBootstrap(opts = {}) {
|
|
|
260
284
|
}
|
|
261
285
|
results.push({
|
|
262
286
|
component: name,
|
|
263
|
-
command: item.command,
|
|
287
|
+
command: redactSecrets(item.command),
|
|
264
288
|
ok: exitCode === 0,
|
|
265
289
|
exitCode,
|
|
266
290
|
captures,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for this provider's own CLI entry point.
|
|
3
|
+
*
|
|
4
|
+
* Split from `cli.ts` so the rules are testable: importing `cli.ts` runs
|
|
5
|
+
* `main()`, and its helpers read a module-level `args`. This package has no
|
|
6
|
+
* dependency edge to `packages/launchfile`, so the unified CLI's parser cannot
|
|
7
|
+
* be imported — these rules are deliberately duplicated and must stay in step
|
|
8
|
+
* with `packages/launchfile/src/cli-args.ts`, or one Launchfile yields two
|
|
9
|
+
* running topologies depending on which entry point ran it (P-5, D-41).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The D-41 component selector, or undefined when no name is given — an absent
|
|
13
|
+
* flag and an empty value both mean every component. The comma-separated and
|
|
14
|
+
* repeatable forms compose; blanks and duplicates are dropped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseComponentsFlag(args: readonly string[]): string[] | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The refusal a verb owes when the selector appears on it but nothing reads
|
|
19
|
+
* the value. `down` and `status` act on the whole deployment and have no set
|
|
20
|
+
* to narrow, so accepting the flag and ignoring it would stop or report every
|
|
21
|
+
* component while the operator named one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function selectorRefusal(args: readonly string[], verb: string, action: string): readonly [string, string] | undefined;
|
|
24
|
+
//# sourceMappingURL=cli-args.d.ts.map
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for this provider's own CLI entry point.
|
|
3
|
+
*
|
|
4
|
+
* Split from `cli.ts` so the rules are testable: importing `cli.ts` runs
|
|
5
|
+
* `main()`, and its helpers read a module-level `args`. This package has no
|
|
6
|
+
* dependency edge to `packages/launchfile`, so the unified CLI's parser cannot
|
|
7
|
+
* be imported — these rules are deliberately duplicated and must stay in step
|
|
8
|
+
* with `packages/launchfile/src/cli-args.ts`, or one Launchfile yields two
|
|
9
|
+
* running topologies depending on which entry point ran it (P-5, D-41).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The D-41 component selector, or undefined when no name is given — an absent
|
|
13
|
+
* flag and an empty value both mean every component. The comma-separated and
|
|
14
|
+
* repeatable forms compose; blanks and duplicates are dropped.
|
|
15
|
+
*/
|
|
16
|
+
export function parseComponentsFlag(args) {
|
|
17
|
+
const names = [];
|
|
18
|
+
const inlinePrefix = "--components=";
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
let raw;
|
|
22
|
+
if (arg === "--components")
|
|
23
|
+
raw = args[i + 1];
|
|
24
|
+
else if (arg?.startsWith(inlinePrefix))
|
|
25
|
+
raw = arg.slice(inlinePrefix.length);
|
|
26
|
+
if (raw === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
for (const part of raw.split(",")) {
|
|
29
|
+
const name = part.trim();
|
|
30
|
+
if (name.length > 0 && !names.includes(name))
|
|
31
|
+
names.push(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return names.length > 0 ? names : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The refusal a verb owes when the selector appears on it but nothing reads
|
|
38
|
+
* the value. `down` and `status` act on the whole deployment and have no set
|
|
39
|
+
* to narrow, so accepting the flag and ignoring it would stop or report every
|
|
40
|
+
* component while the operator named one.
|
|
41
|
+
*/
|
|
42
|
+
export function selectorRefusal(args, verb, action) {
|
|
43
|
+
const present = args.some((arg) => arg === "--components" || arg.startsWith("--components="));
|
|
44
|
+
if (!present)
|
|
45
|
+
return undefined;
|
|
46
|
+
return [
|
|
47
|
+
`--components selects which components \`up\` starts; \`${verb}\` ${action}.`,
|
|
48
|
+
`Run \`${verb}\` with no selector.`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=cli-args.js.map
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CLI entry point for the macOS dev provider.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* launch up [--with-optional] [--no-build] [--dry-run]
|
|
6
|
+
* launch up [--with-optional] [--no-build] [--dry-run] [--components <a,b>]
|
|
7
7
|
* launch down [--destroy]
|
|
8
8
|
* launch status
|
|
9
9
|
* launch env [component]
|
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* CLI entry point for the macOS dev provider.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* launch up [--with-optional] [--no-build] [--dry-run]
|
|
6
|
+
* launch up [--with-optional] [--no-build] [--dry-run] [--components <a,b>]
|
|
7
7
|
* launch down [--destroy]
|
|
8
8
|
* launch status
|
|
9
9
|
* launch env [component]
|
|
10
10
|
*/
|
|
11
11
|
import { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
|
|
12
|
+
import { parseComponentsFlag, selectorRefusal } from "./cli-args.js";
|
|
12
13
|
const args = process.argv.slice(2);
|
|
13
14
|
const command = args[0];
|
|
14
15
|
function hasFlag(flag) {
|
|
@@ -17,6 +18,19 @@ function hasFlag(flag) {
|
|
|
17
18
|
function getArg(index) {
|
|
18
19
|
return args[index];
|
|
19
20
|
}
|
|
21
|
+
/** The D-41 component selector for this invocation. */
|
|
22
|
+
function componentsFlag() {
|
|
23
|
+
return parseComponentsFlag(args);
|
|
24
|
+
}
|
|
25
|
+
/** Exit 1 when the selector reaches a verb that acts on the whole deployment. */
|
|
26
|
+
function refuseSelector(verb, action) {
|
|
27
|
+
const lines = selectorRefusal(args, verb, action);
|
|
28
|
+
if (lines === undefined)
|
|
29
|
+
return;
|
|
30
|
+
for (const line of lines)
|
|
31
|
+
console.error(line);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
20
34
|
async function main() {
|
|
21
35
|
switch (command) {
|
|
22
36
|
case "up":
|
|
@@ -25,14 +39,17 @@ async function main() {
|
|
|
25
39
|
noBuild: hasFlag("no-build"),
|
|
26
40
|
dryRun: hasFlag("dry-run"),
|
|
27
41
|
detach: hasFlag("detach"),
|
|
42
|
+
components: componentsFlag(),
|
|
28
43
|
});
|
|
29
44
|
break;
|
|
30
45
|
case "down":
|
|
46
|
+
refuseSelector("down", "stops the whole deployment");
|
|
31
47
|
await launchDown({
|
|
32
48
|
destroy: hasFlag("destroy"),
|
|
33
49
|
});
|
|
34
50
|
break;
|
|
35
51
|
case "status":
|
|
52
|
+
refuseSelector("status", "reports the whole deployment");
|
|
36
53
|
await launchStatus();
|
|
37
54
|
break;
|
|
38
55
|
case "env":
|
|
@@ -44,8 +61,10 @@ async function main() {
|
|
|
44
61
|
console.log(`launch — macOS dev provider for Launchfile
|
|
45
62
|
|
|
46
63
|
Usage:
|
|
47
|
-
launch up [--with-optional] [--no-build] [--dry-run]
|
|
64
|
+
launch up [--with-optional] [--no-build] [--dry-run] [--components <a,b>]
|
|
48
65
|
Provision resources, install deps, and start the app.
|
|
66
|
+
--components starts only the named components plus their downward
|
|
67
|
+
dependency closure (D-41); omit it to start every component.
|
|
49
68
|
|
|
50
69
|
launch down [--destroy]
|
|
51
70
|
Stop processes. --destroy also drops databases and cleans up.
|
package/dist/env-writer.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* then writes the results to .env files.
|
|
6
6
|
*/
|
|
7
7
|
import { type AppEndpointProperties, type NormalizedComponent, type NormalizedLaunch, type ResolverContext, type Secret, type UnsuppliedRequiredEnv } from "@launchfile/sdk";
|
|
8
|
+
import { type DeclaredPrimary } from "./https-origin.js";
|
|
8
9
|
import type { ResourceProperties } from "./resources/types.js";
|
|
9
10
|
import { type DbIndexes } from "./resources/uses.js";
|
|
10
11
|
import { type LaunchState } from "./state.js";
|
|
@@ -21,6 +22,14 @@ export type { ResolverContext, UnsuppliedRequiredEnv };
|
|
|
21
22
|
* Apps with no exposed component get `port: 0` and `url: ""` (and empty
|
|
22
23
|
* authority/scheme/tls).
|
|
23
24
|
*
|
|
25
|
+
* A declared primary whose component `up` refuses — a `requires:` entry the
|
|
26
|
+
* publication context does not satisfy — keeps its place and has no address:
|
|
27
|
+
* every property is `REFUSED_PRIMARY_ADDRESS` (D-72), never a surviving
|
|
28
|
+
* sibling's port and never the URL that failed to satisfy the entry. `up`
|
|
29
|
+
* passes `primary` as it read it before the refusal removed the component
|
|
30
|
+
* from `launch.components`; `env` and `bootstrap` read the file whole and let
|
|
31
|
+
* the default compute it, so the three answer alike.
|
|
32
|
+
*
|
|
24
33
|
* With an `appUrl` — the orchestrator-supplied publication context (D-58) —
|
|
25
34
|
* routing has moved upstream and the supplied URL answers instead, via the
|
|
26
35
|
* SDK's `suppliedAppProperties`: the same derivation `@launchfile/docker` uses,
|
|
@@ -38,7 +47,39 @@ export type { ResolverContext, UnsuppliedRequiredEnv };
|
|
|
38
47
|
* provider allocates one port per component, so the named endpoint's address
|
|
39
48
|
* is its component's port.
|
|
40
49
|
*/
|
|
41
|
-
export declare function computeAppProperties(launch: NormalizedLaunch, componentPorts: Record<string, number>, appUrl?: string): Record<string, string | number>;
|
|
50
|
+
export declare function computeAppProperties(launch: NormalizedLaunch, componentPorts: Record<string, number>, appUrl?: string, primary?: DeclaredPrimary | undefined): Record<string, string | number>;
|
|
51
|
+
/**
|
|
52
|
+
* The component whose port is the app's primary endpoint — the one `$app.*`
|
|
53
|
+
* reads and the one a supplied publication URL asserts (D-58 rule 4): the
|
|
54
|
+
* component that declares an `https-origin` entry (D-60 rule 3 — declaration
|
|
55
|
+
* fixes the primary, fulfilled or not), else the first component in
|
|
56
|
+
* declaration order that has an `exposed: true` provides entry and an
|
|
57
|
+
* allocated port. `undefined` when the app publishes nothing, and when the
|
|
58
|
+
* declared component has no allocated port (the answer is always a
|
|
59
|
+
* `componentPorts` key or nothing). `up` records
|
|
60
|
+
* the answer in state so `status`, which never reads the Launchfile, places
|
|
61
|
+
* the supplied URL on the same component.
|
|
62
|
+
*/
|
|
63
|
+
export declare function primaryComponent(launch: NormalizedLaunch, componentPorts: Record<string, number>, primary?: DeclaredPrimary | undefined): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* The `ports` key `up` and `status` print the supplied publication URL on
|
|
66
|
+
* (§7): the primary component, when a declared `https-origin` names its
|
|
67
|
+
* endpoint — that entry's `url` is the `https` origin for every listener it
|
|
68
|
+
* admits, `ws` and `grpc` included (D-60 rule 4) — or, with no such entry,
|
|
69
|
+
* when its first `exposed: true` entry's effective listener is `http` or
|
|
70
|
+
* `https`. `undefined` for a positional `ws`, `tcp`, `udp` or `grpc`
|
|
71
|
+
* primary: a supplied URL is an `http`/`https` address and asserts nothing
|
|
72
|
+
* about what those listeners speak (D-58 rule 2), so that key keeps this
|
|
73
|
+
* provider's own printed form while `$app.url` still reads the supplied URL.
|
|
74
|
+
* This provider activates no certificate, so the effective protocol is the
|
|
75
|
+
* declared one.
|
|
76
|
+
*
|
|
77
|
+
* `undefined` for a refused declared primary (D-72): it has no address and
|
|
78
|
+
* no `ports` key, and no surviving sibling takes its place. `up` passes
|
|
79
|
+
* `primary` as it read it before its refusals removed the component from
|
|
80
|
+
* `launch.components`, as for `computeAppProperties`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function printedPrimaryEndpoint(launch: NormalizedLaunch, componentPorts: Record<string, number>, appUrl?: string, primary?: DeclaredPrimary | undefined): string | undefined;
|
|
42
83
|
/**
|
|
43
84
|
* `$app.endpoints.<name>.*` under this provider (D-63 rule 4): every
|
|
44
85
|
* property of every named published endpoint resolves `""`. The allocator
|
|
@@ -101,9 +142,11 @@ export declare function resourceMapFromState(launch: NormalizedLaunch, state: La
|
|
|
101
142
|
* inputs: the registered resources, the recorded ports and secrets, `$app.*`
|
|
102
143
|
* from the recorded publication context (D-58) with a satisfied
|
|
103
144
|
* `https-origin` wired to the same string (D-60 rule 4), and the declared
|
|
104
|
-
* uses the resolver applies strictly.
|
|
145
|
+
* uses the resolver applies strictly. `primary` is the declared primary as
|
|
146
|
+
* `up` read it before its refusals; the other two verbs omit it and read the
|
|
147
|
+
* whole file.
|
|
105
148
|
*/
|
|
106
|
-
export declare function resolverContextFor(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, state: LaunchState): ResolverContext;
|
|
149
|
+
export declare function resolverContextFor(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, state: LaunchState, primary?: DeclaredPrimary): ResolverContext;
|
|
107
150
|
/**
|
|
108
151
|
* The resolved environment for one component, plus what the file did not supply.
|
|
109
152
|
*/
|
package/dist/env-writer.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { deriveAppUrlProperties, endpointProperties, resolveExpression, isExpression, parseUseKey, suppliedAppProperties, UNPUBLISHED_APP_ENDPOINT, unsuppliedRequiredEnv, useKeys, } from "@launchfile/sdk";
|
|
10
|
-
import {
|
|
9
|
+
import { deriveAppUrlProperties, effectiveListener, endpointProperties, resolveExpression, isExpression, parseUseKey, suppliedAppProperties, UNPUBLISHED_APP_ENDPOINT, unsuppliedRequiredEnv, useKeys, } from "@launchfile/sdk";
|
|
10
|
+
import { declaredPrimary, REFUSED_PRIMARY_ADDRESS, wireHttpsOrigins, } from "./https-origin.js";
|
|
11
11
|
import { getProvisioner } from "./resources/index.js";
|
|
12
12
|
import { coveredUses, namedDatabases, withCoveredUses } from "./resources/uses.js";
|
|
13
13
|
import { generateValue } from "./secret-generator.js";
|
|
@@ -24,6 +24,14 @@ import { recordedDbIndexes } from "./state.js";
|
|
|
24
24
|
* Apps with no exposed component get `port: 0` and `url: ""` (and empty
|
|
25
25
|
* authority/scheme/tls).
|
|
26
26
|
*
|
|
27
|
+
* A declared primary whose component `up` refuses — a `requires:` entry the
|
|
28
|
+
* publication context does not satisfy — keeps its place and has no address:
|
|
29
|
+
* every property is `REFUSED_PRIMARY_ADDRESS` (D-72), never a surviving
|
|
30
|
+
* sibling's port and never the URL that failed to satisfy the entry. `up`
|
|
31
|
+
* passes `primary` as it read it before the refusal removed the component
|
|
32
|
+
* from `launch.components`; `env` and `bootstrap` read the file whole and let
|
|
33
|
+
* the default compute it, so the three answer alike.
|
|
34
|
+
*
|
|
27
35
|
* With an `appUrl` — the orchestrator-supplied publication context (D-58) —
|
|
28
36
|
* routing has moved upstream and the supplied URL answers instead, via the
|
|
29
37
|
* SDK's `suppliedAppProperties`: the same derivation `@launchfile/docker` uses,
|
|
@@ -41,28 +49,13 @@ import { recordedDbIndexes } from "./state.js";
|
|
|
41
49
|
* provider allocates one port per component, so the named endpoint's address
|
|
42
50
|
* is its component's port.
|
|
43
51
|
*/
|
|
44
|
-
export function computeAppProperties(launch, componentPorts, appUrl) {
|
|
52
|
+
export function computeAppProperties(launch, componentPorts, appUrl, primary = declaredPrimary(launch, appUrl)) {
|
|
53
|
+
if (primary?.refused)
|
|
54
|
+
return { name: launch.name, ...REFUSED_PRIMARY_ADDRESS };
|
|
45
55
|
if (appUrl !== undefined)
|
|
46
56
|
return suppliedAppProperties(launch.name, appUrl);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (declared !== undefined) {
|
|
50
|
-
// A declared `https-origin` names the primary explicitly, so the
|
|
51
|
-
// positional answer below does not run — the point of D-60 rule 3. The
|
|
52
|
-
// SDK requires the named endpoint to be `exposed: true` on this component.
|
|
53
|
-
primaryPort = componentPorts[declared] ?? 0;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
for (const [name, component] of Object.entries(launch.components)) {
|
|
57
|
-
// Only endpoints explicitly marked `exposed: true` are reachable from
|
|
58
|
-
// outside the host (D-27), so only they can be the app's public address.
|
|
59
|
-
const hasExposed = component.provides?.some((p) => p.exposed === true) ?? false;
|
|
60
|
-
if (hasExposed && componentPorts[name]) {
|
|
61
|
-
primaryPort = componentPorts[name];
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
57
|
+
const component = primaryComponent(launch, componentPorts, primary);
|
|
58
|
+
const primaryPort = component === undefined ? 0 : (componentPorts[component] ?? 0);
|
|
66
59
|
const url = primaryPort > 0 ? `http://localhost:${primaryPort}` : "";
|
|
67
60
|
return {
|
|
68
61
|
name: launch.name,
|
|
@@ -72,6 +65,69 @@ export function computeAppProperties(launch, componentPorts, appUrl) {
|
|
|
72
65
|
...deriveAppUrlProperties(url),
|
|
73
66
|
};
|
|
74
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The component whose port is the app's primary endpoint — the one `$app.*`
|
|
70
|
+
* reads and the one a supplied publication URL asserts (D-58 rule 4): the
|
|
71
|
+
* component that declares an `https-origin` entry (D-60 rule 3 — declaration
|
|
72
|
+
* fixes the primary, fulfilled or not), else the first component in
|
|
73
|
+
* declaration order that has an `exposed: true` provides entry and an
|
|
74
|
+
* allocated port. `undefined` when the app publishes nothing, and when the
|
|
75
|
+
* declared component has no allocated port (the answer is always a
|
|
76
|
+
* `componentPorts` key or nothing). `up` records
|
|
77
|
+
* the answer in state so `status`, which never reads the Launchfile, places
|
|
78
|
+
* the supplied URL on the same component.
|
|
79
|
+
*/
|
|
80
|
+
export function primaryComponent(launch, componentPorts, primary = declaredPrimary(launch)) {
|
|
81
|
+
if (primary !== undefined) {
|
|
82
|
+
// A declared `https-origin` names the primary explicitly, so the
|
|
83
|
+
// positional answer below does not run — the point of D-60 rule 3. The
|
|
84
|
+
// SDK requires the named endpoint to be `exposed: true` on this component.
|
|
85
|
+
// Without an allocated port it is still the primary, so the search stays
|
|
86
|
+
// off — but it is not a `ports` key, and `primaryEndpoint` must name one
|
|
87
|
+
// or nothing, so answer `undefined` rather than a key `state.ports` lacks.
|
|
88
|
+
return componentPorts[primary.component] ? primary.component : undefined;
|
|
89
|
+
}
|
|
90
|
+
for (const [name, component] of Object.entries(launch.components)) {
|
|
91
|
+
// Only endpoints explicitly marked `exposed: true` are reachable from
|
|
92
|
+
// outside the host (D-27), so only they can be the app's public address.
|
|
93
|
+
const hasExposed = component.provides?.some((p) => p.exposed === true) ?? false;
|
|
94
|
+
if (hasExposed && componentPorts[name])
|
|
95
|
+
return name;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The `ports` key `up` and `status` print the supplied publication URL on
|
|
101
|
+
* (§7): the primary component, when a declared `https-origin` names its
|
|
102
|
+
* endpoint — that entry's `url` is the `https` origin for every listener it
|
|
103
|
+
* admits, `ws` and `grpc` included (D-60 rule 4) — or, with no such entry,
|
|
104
|
+
* when its first `exposed: true` entry's effective listener is `http` or
|
|
105
|
+
* `https`. `undefined` for a positional `ws`, `tcp`, `udp` or `grpc`
|
|
106
|
+
* primary: a supplied URL is an `http`/`https` address and asserts nothing
|
|
107
|
+
* about what those listeners speak (D-58 rule 2), so that key keeps this
|
|
108
|
+
* provider's own printed form while `$app.url` still reads the supplied URL.
|
|
109
|
+
* This provider activates no certificate, so the effective protocol is the
|
|
110
|
+
* declared one.
|
|
111
|
+
*
|
|
112
|
+
* `undefined` for a refused declared primary (D-72): it has no address and
|
|
113
|
+
* no `ports` key, and no surviving sibling takes its place. `up` passes
|
|
114
|
+
* `primary` as it read it before its refusals removed the component from
|
|
115
|
+
* `launch.components`, as for `computeAppProperties`.
|
|
116
|
+
*/
|
|
117
|
+
export function printedPrimaryEndpoint(launch, componentPorts, appUrl, primary = declaredPrimary(launch, appUrl)) {
|
|
118
|
+
if (primary?.refused)
|
|
119
|
+
return undefined;
|
|
120
|
+
const component = primaryComponent(launch, componentPorts, primary);
|
|
121
|
+
if (component === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (primary !== undefined)
|
|
124
|
+
return component;
|
|
125
|
+
const entry = launch.components[component]?.provides?.find((p) => p.exposed === true);
|
|
126
|
+
if (entry === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const { protocol } = effectiveListener(entry);
|
|
129
|
+
return protocol === "http" || protocol === "https" ? component : undefined;
|
|
130
|
+
}
|
|
75
131
|
/**
|
|
76
132
|
* `$app.endpoints.<name>.*` under this provider (D-63 rule 4): every
|
|
77
133
|
* property of every named published endpoint resolves `""`. The allocator
|
|
@@ -215,10 +271,12 @@ export async function resourceMapFromState(launch, state, projectDir) {
|
|
|
215
271
|
* inputs: the registered resources, the recorded ports and secrets, `$app.*`
|
|
216
272
|
* from the recorded publication context (D-58) with a satisfied
|
|
217
273
|
* `https-origin` wired to the same string (D-60 rule 4), and the declared
|
|
218
|
-
* uses the resolver applies strictly.
|
|
274
|
+
* uses the resolver applies strictly. `primary` is the declared primary as
|
|
275
|
+
* `up` read it before its refusals; the other two verbs omit it and read the
|
|
276
|
+
* whole file.
|
|
219
277
|
*/
|
|
220
|
-
export function resolverContextFor(launch, resourceMap, state) {
|
|
221
|
-
const appProperties = computeAppProperties(launch, state.ports, state.appUrl);
|
|
278
|
+
export function resolverContextFor(launch, resourceMap, state, primary) {
|
|
279
|
+
const appProperties = computeAppProperties(launch, state.ports, state.appUrl, primary);
|
|
222
280
|
wireHttpsOrigins(launch, resourceMap, state.appUrl);
|
|
223
281
|
return buildResolverContext(resourceMap, state.ports, state.secrets, appProperties, computeAppEndpoints(launch), declaredUses(launch), launch.components);
|
|
224
282
|
}
|
package/dist/health.d.ts
CHANGED
|
@@ -10,9 +10,30 @@ import { type NormalizedHealth } from "@launchfile/sdk";
|
|
|
10
10
|
* `interval: "1h"` became a zero-length poll that could never pass.
|
|
11
11
|
*/
|
|
12
12
|
export declare function parseDuration(duration: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a check can run without a port: only a `command` check can. A
|
|
15
|
+
* `path` check and the no-check fallback both poll `http://localhost:<port>`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function healthCheckNeedsPort(health: NormalizedHealth): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* What a check polls, for the failure message: the operator reading
|
|
20
|
+
* "did not become healthy" needs to know which probe was asked.
|
|
21
|
+
*/
|
|
22
|
+
export declare function describeHealthCheck(health: NormalizedHealth, port: number | undefined): string;
|
|
23
|
+
/**
|
|
24
|
+
* How long a check gets before the component counts as never healthy. A
|
|
25
|
+
* declared `retries` is the file's own window — that many consecutive failures,
|
|
26
|
+
* each costing up to one `timeout` plus one `interval` (SPEC.md § health, the
|
|
27
|
+
* window docker's compose healthcheck gives it). The provider default fills in
|
|
28
|
+
* only when the file declares no `retries`: PROVIDERS.md §10 rule 10 keeps
|
|
29
|
+
* defaults for absent values, never over declared ones. `start_period` is
|
|
30
|
+
* waited in full before the window opens and is not part of it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function healthBudgetMs(health: NormalizedHealth, fallbackMs: number): number;
|
|
13
33
|
/**
|
|
14
34
|
* Wait for a component to become healthy.
|
|
15
|
-
* Returns true if healthy, false if timed out.
|
|
35
|
+
* Returns true if healthy, false if timed out. `overallTimeout` is the polling
|
|
36
|
+
* window after `start_period`; callers derive it with `healthBudgetMs`.
|
|
16
37
|
*/
|
|
17
38
|
export declare function waitForHealthy(name: string, health: NormalizedHealth, port: number, overallTimeout?: number): Promise<boolean>;
|
|
18
39
|
//# sourceMappingURL=health.d.ts.map
|
package/dist/health.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Health check polling for components.
|
|
3
3
|
*/
|
|
4
4
|
import { parseDurationMs } from "@launchfile/sdk";
|
|
5
|
+
import { redactSecrets } from "./redact.js";
|
|
5
6
|
import { shellScript } from "./shell.js";
|
|
6
7
|
/**
|
|
7
8
|
* Parse a health duration against the ratified grammar (D-48). Throws on an
|
|
@@ -13,9 +14,43 @@ import { shellScript } from "./shell.js";
|
|
|
13
14
|
export function parseDuration(duration) {
|
|
14
15
|
return parseDurationMs(duration);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether a check can run without a port: only a `command` check can. A
|
|
19
|
+
* `path` check and the no-check fallback both poll `http://localhost:<port>`.
|
|
20
|
+
*/
|
|
21
|
+
export function healthCheckNeedsPort(health) {
|
|
22
|
+
return !health.command;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What a check polls, for the failure message: the operator reading
|
|
26
|
+
* "did not become healthy" needs to know which probe was asked.
|
|
27
|
+
*/
|
|
28
|
+
export function describeHealthCheck(health, port) {
|
|
29
|
+
if (health.command)
|
|
30
|
+
return `command \`${redactSecrets(health.command)}\``;
|
|
31
|
+
const host = port === undefined ? "localhost:<unallocated>" : `localhost:${port}`;
|
|
32
|
+
return `GET http://${host}${health.path ?? "/"}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* How long a check gets before the component counts as never healthy. A
|
|
36
|
+
* declared `retries` is the file's own window — that many consecutive failures,
|
|
37
|
+
* each costing up to one `timeout` plus one `interval` (SPEC.md § health, the
|
|
38
|
+
* window docker's compose healthcheck gives it). The provider default fills in
|
|
39
|
+
* only when the file declares no `retries`: PROVIDERS.md §10 rule 10 keeps
|
|
40
|
+
* defaults for absent values, never over declared ones. `start_period` is
|
|
41
|
+
* waited in full before the window opens and is not part of it.
|
|
42
|
+
*/
|
|
43
|
+
export function healthBudgetMs(health, fallbackMs) {
|
|
44
|
+
if (health.retries === undefined)
|
|
45
|
+
return fallbackMs;
|
|
46
|
+
const interval = parseDuration(health.interval ?? "3s");
|
|
47
|
+
const checkTimeout = parseDuration(health.timeout ?? "5s");
|
|
48
|
+
return health.retries * (interval + checkTimeout);
|
|
49
|
+
}
|
|
16
50
|
/**
|
|
17
51
|
* Wait for a component to become healthy.
|
|
18
|
-
* Returns true if healthy, false if timed out.
|
|
52
|
+
* Returns true if healthy, false if timed out. `overallTimeout` is the polling
|
|
53
|
+
* window after `start_period`; callers derive it with `healthBudgetMs`.
|
|
19
54
|
*/
|
|
20
55
|
export async function waitForHealthy(name, health, port, overallTimeout = 60_000) {
|
|
21
56
|
const startPeriod = parseDuration(health.start_period ?? "0s");
|
package/dist/https-origin.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* D-56 rule 3 stands — the provider does not verify the origin exists or is
|
|
11
11
|
* ready.
|
|
12
12
|
*/
|
|
13
|
-
import { type NormalizedLaunch, type NormalizedRequirement } from "@launchfile/sdk";
|
|
13
|
+
import { type AppEndpointProperties, type NormalizedLaunch, type NormalizedRequirement } from "@launchfile/sdk";
|
|
14
14
|
import type { ResourceProperties } from "./resources/types.js";
|
|
15
15
|
/** The backing-service type that declares the app's public HTTPS origin (D-60). */
|
|
16
16
|
export declare const HTTPS_ORIGIN = "https-origin";
|
|
@@ -21,6 +21,12 @@ export declare const HTTPS_ORIGIN = "https-origin";
|
|
|
21
21
|
* `launchUp` records it.
|
|
22
22
|
*/
|
|
23
23
|
export declare function httpsOriginSatisfied(appUrl: string | undefined): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The uses an `https-origin` entry declares that this provider cannot cover,
|
|
26
|
+
* spelled as the file spells them. Asked of the use registry, never assumed:
|
|
27
|
+
* a use registered for the type later is covered with no change here.
|
|
28
|
+
*/
|
|
29
|
+
export declare function uncoveredOriginUses(entry: NormalizedRequirement): string[];
|
|
24
30
|
/**
|
|
25
31
|
* Why one `https-origin` entry is not satisfied, for a refusal or a degraded
|
|
26
32
|
* note — the same two reasons `@launchfile/docker` gives, so one Launchfile
|
|
@@ -28,21 +34,51 @@ export declare function httpsOriginSatisfied(appUrl: string | undefined): boolea
|
|
|
28
34
|
*/
|
|
29
35
|
export declare function httpsOriginShortfall(entry: NormalizedRequirement, appUrl: string | undefined): string;
|
|
30
36
|
/**
|
|
31
|
-
* The
|
|
37
|
+
* The `$app.*` address of a primary whose component is refused (D-72):
|
|
38
|
+
* every field `""` — the answer D-63 rule 4 gives an endpoint the provider
|
|
39
|
+
* publishes no address for — with `tls` reading `false`, as D-63 rule 3 has
|
|
40
|
+
* a listener with no origin read it, so a literal on/off flag
|
|
41
|
+
* (`USE_SSL: $app.tls`) still receives a boolean. The same object
|
|
42
|
+
* `@launchfile/docker` resolves (P-5).
|
|
43
|
+
*/
|
|
44
|
+
export declare const REFUSED_PRIMARY_ADDRESS: Readonly<AppEndpointProperties>;
|
|
45
|
+
/** The primary an `https-origin` entry declares (D-60 rule 3). */
|
|
46
|
+
export interface DeclaredPrimary {
|
|
47
|
+
/** The component the entry sits on — the one that owns the named endpoint. */
|
|
48
|
+
component: string;
|
|
49
|
+
/**
|
|
50
|
+
* The entry is `requires:` and the publication context does not satisfy
|
|
51
|
+
* it, so `up` refuses the component (D-60 rule 5). It stays the primary; it
|
|
52
|
+
* has no address (D-72).
|
|
53
|
+
*/
|
|
54
|
+
refused: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The primary an `https-origin` entry declares, when the file declares one
|
|
32
58
|
* (D-60 rule 3), else `undefined`. **Declaration** fixes the primary, not
|
|
33
59
|
* fulfillment: a `supports:` entry this provider leaves unsatisfied still
|
|
34
|
-
* names it, so
|
|
35
|
-
*
|
|
36
|
-
*
|
|
60
|
+
* names it, and so does a `requires:` entry whose component this provider
|
|
61
|
+
* refuses — `refused` says which, and `computeAppProperties` then resolves
|
|
62
|
+
* the empty address rather than a surviving sibling's (D-72). Either way
|
|
63
|
+
* `$app.*` does not move with the provider's capability. The SDK caps the app
|
|
64
|
+
* at one such entry and requires it to sit on the component that owns the
|
|
65
|
+
* named endpoint, so the first match is the only one.
|
|
66
|
+
*
|
|
67
|
+
* `up` reads this before its refusals remove anything from
|
|
68
|
+
* `launch.components`; `env` and `bootstrap` read the file whole. `appUrl` is
|
|
69
|
+
* the effective publication context — supplied or recorded — normalized.
|
|
37
70
|
*/
|
|
38
|
-
export declare function
|
|
71
|
+
export declare function declaredPrimary(launch: NormalizedLaunch, appUrl?: string): DeclaredPrimary | undefined;
|
|
39
72
|
/**
|
|
40
73
|
* Register every satisfied `https-origin` entry as a resource so its `set_env`
|
|
41
74
|
* resolves. One registered property, `url` (D-60 rule 4), holding the same
|
|
42
|
-
* string as `$app.url
|
|
43
|
-
*
|
|
44
|
-
* `set_env` stays absent (never `""`)
|
|
45
|
-
* provider did not provision.
|
|
75
|
+
* string as `$app.url`, plus the properties of each declared use. Mutates
|
|
76
|
+
* `resourceMap`; a no-op when the recorded publication URL does not satisfy
|
|
77
|
+
* the type, so an unsatisfied entry's `set_env` stays absent (never `""`)
|
|
78
|
+
* exactly as for any other resource this provider did not provision. An entry
|
|
79
|
+
* declaring a use this provider cannot cover is unsatisfied the same way
|
|
80
|
+
* (D-65): a `supports:` entry runs degraded, and a `requires:` one refused its
|
|
81
|
+
* component before launch.
|
|
46
82
|
*/
|
|
47
83
|
export declare function wireHttpsOrigins(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, appUrl: string | undefined): void;
|
|
48
84
|
//# sourceMappingURL=https-origin.d.ts.map
|