@launchfile/macos-dev 0.11.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.
@@ -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
- // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI match
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
@@ -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.
@@ -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
  */
@@ -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 { declaredPrimaryComponent, wireHttpsOrigins } from "./https-origin.js";
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
- let primaryPort = 0;
48
- const declared = declaredPrimaryComponent(launch);
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");
@@ -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 component an `https-origin` entry sits on, when the file declares one
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 `$app.*` does not change value with the provider's capability.
35
- * The SDK caps the app at one such entry and requires it to sit on the
36
- * component that owns the named endpoint, so the first match is the only one.
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 declaredPrimaryComponent(launch: NormalizedLaunch): string | undefined;
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`. Mutates `resourceMap`; a no-op when the recorded
43
- * publication URL does not satisfy the type, so an unsatisfied entry's
44
- * `set_env` stays absent (never `""`) exactly as for any other resource this
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