@launchfile/macos-dev 0.8.0 → 0.10.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.
@@ -101,6 +101,13 @@ export type BootstrapExec = (cmd: string, args: string[], opts: {
101
101
  export declare function launchBootstrap(opts?: {
102
102
  component?: string;
103
103
  projectDir?: string;
104
+ /**
105
+ * Print sensitive captures instead of masking them — the operator's
106
+ * explicit act on the invoking command (`launchfile bootstrap
107
+ * --reveal`). Display only: the values are registered with the
108
+ * redactor either way.
109
+ */
110
+ reveal?: boolean;
104
111
  exec?: BootstrapExec;
105
112
  }): Promise<BootstrapResult[]>;
106
113
  //# sourceMappingURL=bootstrap.d.ts.map
package/dist/bootstrap.js CHANGED
@@ -16,11 +16,10 @@
16
16
  import { readFile } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
18
  import { spawn } from "node:child_process";
19
- import { parseDurationMs, readLaunch, resolveExpression, } from "@launchfile/sdk";
19
+ import { formatCaptures, parseDurationMs, readLaunch, resolveExpression, sensitiveCaptureValues, } from "@launchfile/sdk";
20
20
  import { loadState, saveState } from "./state.js";
21
- import { buildResolverContext, computeAppProperties, resolveComponentEnv, resolveGenerators, } from "./env-writer.js";
22
- import { redactSecrets } from "./redact.js";
23
- import { getProvisioner } from "./resources/index.js";
21
+ import { resolveComponentEnv, resolveGenerators, resolverContextFor, resourceMapFromState, } from "./env-writer.js";
22
+ import { redactSecrets, registerDeclaredSecret } from "./redact.js";
24
23
  /** Default budget for a bootstrap command when no `timeout` is declared. */
25
24
  export const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 120_000;
26
25
  /**
@@ -184,29 +183,20 @@ const defaultExec = (cmd, args, opts) => new Promise((resolveP) => {
184
183
  */
185
184
  export async function launchBootstrap(opts = {}) {
186
185
  const projectDir = opts.projectDir ?? process.cwd();
186
+ const reveal = opts.reveal === true;
187
187
  const launchfileContent = await readFile(join(projectDir, "Launchfile"), "utf8");
188
188
  const launch = readLaunch(launchfileContent);
189
189
  const state = await loadState(projectDir);
190
190
  if (!state) {
191
191
  throw new Error("No active launch state. Run `launch up` first.");
192
192
  }
193
- // Rebuild resource map from state so the resolver context has real
194
- // values at invocation time (same pattern as launchEnv). This calls
195
- // provisioner.provision() on already-provisioned resources to retrieve
196
- // their current property values — relies on every provisioner being
197
- // idempotent: re-running provision() must not corrupt state or
198
- // re-create the resource. All current provisioners satisfy this; new
199
- // provisioners must as well.
200
- const resourceMap = {};
201
- for (const [name, res] of Object.entries(state.resources)) {
202
- const provisioner = getProvisioner(res.type);
203
- if (provisioner) {
204
- const result = await provisioner.provision({ type: res.type, name: res.name }, { appName: state.appName, projectDir }, res);
205
- resourceMap[name] = result.properties;
206
- }
207
- }
208
- const appProperties = computeAppProperties(launch, state.ports);
209
- const context = buildResolverContext(resourceMap, state.ports, state.secrets, appProperties);
193
+ // A bootstrap command reads the same values the app's env was written
194
+ // with: the resources registered as `up` registered them — declared uses
195
+ // included — and `$app.*` from the publication context the last `up`
196
+ // recorded (D-58), not this provider's localhost answer under an upstream
197
+ // proxy.
198
+ const resourceMap = await resourceMapFromState(launch, state, projectDir);
199
+ const context = resolverContextFor(launch, resourceMap, state);
210
200
  const exec = opts.exec ?? defaultExec;
211
201
  const plan = planBootstraps(launch, context, { component: opts.component });
212
202
  const results = [];
@@ -259,26 +249,31 @@ export async function launchBootstrap(opts = {}) {
259
249
  env,
260
250
  timeoutMs: item.timeoutMs,
261
251
  });
252
+ const captureMeta = item.capture ?? {};
262
253
  const captures = item.capture ? extractCaptures(stdout, item.capture) : {};
254
+ // A `sensitive: true` capture is a declared secret (D-18): it registers
255
+ // before any result or printed line is built, so the value can leak
256
+ // through neither (CWE-532). `--reveal` changes what the display loop
257
+ // below prints, never this.
258
+ for (const value of sensitiveCaptureValues(captures, captureMeta)) {
259
+ registerDeclaredSecret(value);
260
+ }
263
261
  results.push({
264
262
  component: name,
265
263
  command: item.command,
266
264
  ok: exitCode === 0,
267
265
  exitCode,
268
266
  captures,
269
- captureMeta: item.capture ?? {},
267
+ captureMeta,
270
268
  stdout,
271
269
  stderr,
272
270
  });
273
271
  // Print captures inline so the user sees them immediately.
274
- if (Object.keys(captures).length > 0) {
275
- console.log("\n Captured:");
276
- for (const [key, value] of Object.entries(captures)) {
277
- const meta = item.capture?.[key];
278
- const displayValue = meta?.sensitive ? "***" : value;
279
- const desc = meta?.description ? ` — ${meta.description}` : "";
280
- console.log(` ${key}: ${displayValue}${desc}`);
281
- }
272
+ const lines = formatCaptures(captures, captureMeta, reveal);
273
+ if (lines.length > 0) {
274
+ console.log("");
275
+ for (const line of lines)
276
+ console.log(line);
282
277
  }
283
278
  if (exitCode !== 0) {
284
279
  console.error(` \u2717 Bootstrap [${name}] failed with exit code ${exitCode}`);
@@ -4,28 +4,101 @@
4
4
  * Connects provisioned resource properties to the SDK's expression resolver,
5
5
  * then writes the results to .env files.
6
6
  */
7
- import { type NormalizedComponent, type NormalizedLaunch, type ResolverContext, type Secret, type UnsuppliedRequiredEnv } from "@launchfile/sdk";
7
+ import { type AppEndpointProperties, type NormalizedComponent, type NormalizedLaunch, type ResolverContext, type Secret, type UnsuppliedRequiredEnv } from "@launchfile/sdk";
8
8
  import type { ResourceProperties } from "./resources/types.js";
9
+ import { type DbIndexes } from "./resources/uses.js";
10
+ import { type LaunchState } from "./state.js";
9
11
  export type { ResolverContext, UnsuppliedRequiredEnv };
10
12
  /**
11
13
  * Compute the $app.* property set (D-33, D-35) for a Launchfile under the
12
- * macos-dev provider. The app's "primary" port comes from the first component
13
- * (in declaration order) that has at least one `exposed: true` provides entry.
14
- * The `authority`/`scheme`/`tls` trio is derived from the resulting URL via the
15
- * SDK so split-field tokens (e.g. `CMD_DOMAIN: $app.authority`) resolve.
14
+ * macos-dev provider.
15
+ *
16
+ * With no `appUrl`, this provider's own routing strategy answers: the app's
17
+ * "primary" port is the port of the component that declares an `https-origin`
18
+ * entry (D-60 rule 3 — declaration fixes the primary, fulfilled or not), else
19
+ * the first component (in declaration order) that has at least one
20
+ * `exposed: true` provides entry, and `http://localhost:<port>` is the address.
16
21
  * Apps with no exposed component get `port: 0` and `url: ""` (and empty
17
22
  * authority/scheme/tls).
18
23
  *
24
+ * With an `appUrl` — the orchestrator-supplied publication context (D-58) —
25
+ * routing has moved upstream and the supplied URL answers instead, via the
26
+ * SDK's `suppliedAppProperties`: the same derivation `@launchfile/docker` uses,
27
+ * so one Launchfile behind one proxy resolves identical `$app.*` under either
28
+ * provider (P-5). The allocated local ports stay orthogonal — still bound, just
29
+ * not the address anyone reaches the app at.
30
+ *
31
+ * Either way the `authority`/`scheme`/`tls` trio is derived from the resulting
32
+ * URL via the SDK so split-field tokens (e.g. `CMD_DOMAIN: $app.authority`)
33
+ * resolve from one definition (D-35).
34
+ *
19
35
  * For multi-exposed-component apps that need a specific component's URL,
20
36
  * use `$components.<name>.url` instead — `$app.*` always points at the
21
- * first exposed component to give a single, predictable answer.
37
+ * primary endpoint to give a single, predictable answer (D-58 rule 4). This
38
+ * provider allocates one port per component, so the named endpoint's address
39
+ * is its component's port.
22
40
  */
23
- export declare function computeAppProperties(launch: NormalizedLaunch, componentPorts: Record<string, number>): Record<string, string | number>;
41
+ export declare function computeAppProperties(launch: NormalizedLaunch, componentPorts: Record<string, number>, appUrl?: string): Record<string, string | number>;
42
+ /**
43
+ * `$app.endpoints.<name>.*` under this provider (D-63 rule 4): every
44
+ * property of every named published endpoint resolves `""`. The allocator
45
+ * hands out one port per **component** (`allocatePorts`, keyed by component
46
+ * name), so a second `exposed: true` entry on a component has no host-side
47
+ * address to publish — not the primary's, and not its own (#294). The
48
+ * primary's entry is `""` too, rather than a copy of `$app.*`, because the
49
+ * per-endpoint form promises a per-endpoint publication this provider does
50
+ * not perform; `$app.*` keeps its own routing answer. Registering the empty
51
+ * answer explicitly, rather than nothing, records that the provider has read
52
+ * the namespace and declined it.
53
+ */
54
+ export declare function computeAppEndpoints(launch: NormalizedLaunch): Record<string, AppEndpointProperties>;
24
55
  /**
25
56
  * Build a ResolverContext from provisioned resources, component ports,
26
- * secrets, and (D-33) the platform-injected app properties.
57
+ * secrets, (D-33) the platform-injected app properties, (D-63) the
58
+ * per-endpoint map — `computeAppEndpoints`, which under this provider is
59
+ * every named published endpoint resolving `""` — and the `uses` each
60
+ * resource entry declares (`declaredUses`), which the resolver reads to
61
+ * resolve `$<resource>.<use>.<property>` strictly.
62
+ */
63
+ export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>, appEndpoints?: Record<string, AppEndpointProperties>, uses?: Record<string, readonly string[]>): ResolverContext;
64
+ /**
65
+ * The use keys each resource entry declares (`db`, `db.cache`), keyed like
66
+ * the resource namespace (`name ?? type`, app-global). Same-name entries pool
67
+ * their keys — D-24 says they describe one resource. Host-capability entries
68
+ * have none.
69
+ */
70
+ export declare function declaredUses(launch: NormalizedLaunch): Record<string, string[]>;
71
+ /**
72
+ * A resource's property map as `up`, `env` and `bootstrap` all register it:
73
+ * the provisioner's instance vocabulary plus every pooled use this provider
74
+ * covers under `<use>.<property>` / `<use>.<name>.<property>` (D-24:
75
+ * same-name entries describe one resource). `dbIndexes` carries the numbered
76
+ * database each redis `db` use key selects. A pooled `db` key with no index
77
+ * in it stays unregistered, so a `$<resource>.db.<property>` (or
78
+ * `$<resource>.db.<name>.<property>`) reference throws `UnresolvedUseError`
79
+ * instead of falling through to the instance url.
80
+ */
81
+ export declare function registerResource(type: string, resourceName: string, uses: Record<string, readonly string[]>, base: ResourceProperties, dbIndexes: DbIndexes): ResourceProperties;
82
+ /**
83
+ * The resource map a subcommand run after `up` (`env`, `bootstrap`) resolves
84
+ * against, rebuilt from state and registered exactly as `up` registered it.
85
+ * Each recorded resource is re-provisioned to read its current properties —
86
+ * every provisioner is idempotent, so this neither re-creates nor corrupts
87
+ * the resource — and a redis `db` use reads back the index `up` recorded
88
+ * rather than one re-derived from the file, which may have changed since. A
89
+ * state file written before the index was recorded leaves `db.*`
90
+ * unregistered, so the strict resolver throws for `$<resource>.db.*` — never
91
+ * the instance url — until the next `up` records it.
92
+ */
93
+ export declare function resourceMapFromState(launch: NormalizedLaunch, state: LaunchState, projectDir: string): Promise<Record<string, ResourceProperties>>;
94
+ /**
95
+ * The resolver context `up`, `env` and `bootstrap` share, built from the same
96
+ * inputs: the registered resources, the recorded ports and secrets, `$app.*`
97
+ * from the recorded publication context (D-58) with a satisfied
98
+ * `https-origin` wired to the same string (D-60 rule 4), and the declared
99
+ * uses the resolver applies strictly.
27
100
  */
28
- export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>): ResolverContext;
101
+ export declare function resolverContextFor(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, state: LaunchState): ResolverContext;
29
102
  /**
30
103
  * The resolved environment for one component, plus what the file did not supply.
31
104
  */
@@ -6,34 +6,61 @@
6
6
  */
7
7
  import { writeFile, mkdir } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { deriveAppUrlProperties, resolveExpression, isExpression, unsuppliedRequiredEnv, } from "@launchfile/sdk";
9
+ import { deriveAppUrlProperties, resolveExpression, isExpression, parseUseKey, suppliedAppProperties, UNPUBLISHED_APP_ENDPOINT, unsuppliedRequiredEnv, useKeys, } from "@launchfile/sdk";
10
+ import { declaredPrimaryComponent, wireHttpsOrigins } from "./https-origin.js";
11
+ import { getProvisioner } from "./resources/index.js";
12
+ import { coveredUses, namedDatabases, withCoveredUses } from "./resources/uses.js";
10
13
  import { generateValue } from "./secret-generator.js";
14
+ import { recordedDbIndexes } from "./state.js";
11
15
  /**
12
16
  * Compute the $app.* property set (D-33, D-35) for a Launchfile under the
13
- * macos-dev provider. The app's "primary" port comes from the first component
14
- * (in declaration order) that has at least one `exposed: true` provides entry.
15
- * The `authority`/`scheme`/`tls` trio is derived from the resulting URL via the
16
- * SDK so split-field tokens (e.g. `CMD_DOMAIN: $app.authority`) resolve.
17
+ * macos-dev provider.
18
+ *
19
+ * With no `appUrl`, this provider's own routing strategy answers: the app's
20
+ * "primary" port is the port of the component that declares an `https-origin`
21
+ * entry (D-60 rule 3 — declaration fixes the primary, fulfilled or not), else
22
+ * the first component (in declaration order) that has at least one
23
+ * `exposed: true` provides entry, and `http://localhost:<port>` is the address.
17
24
  * Apps with no exposed component get `port: 0` and `url: ""` (and empty
18
25
  * authority/scheme/tls).
19
26
  *
27
+ * With an `appUrl` — the orchestrator-supplied publication context (D-58) —
28
+ * routing has moved upstream and the supplied URL answers instead, via the
29
+ * SDK's `suppliedAppProperties`: the same derivation `@launchfile/docker` uses,
30
+ * so one Launchfile behind one proxy resolves identical `$app.*` under either
31
+ * provider (P-5). The allocated local ports stay orthogonal — still bound, just
32
+ * not the address anyone reaches the app at.
33
+ *
34
+ * Either way the `authority`/`scheme`/`tls` trio is derived from the resulting
35
+ * URL via the SDK so split-field tokens (e.g. `CMD_DOMAIN: $app.authority`)
36
+ * resolve from one definition (D-35).
37
+ *
20
38
  * For multi-exposed-component apps that need a specific component's URL,
21
39
  * use `$components.<name>.url` instead — `$app.*` always points at the
22
- * first exposed component to give a single, predictable answer.
40
+ * primary endpoint to give a single, predictable answer (D-58 rule 4). This
41
+ * provider allocates one port per component, so the named endpoint's address
42
+ * is its component's port.
23
43
  */
24
- export function computeAppProperties(launch, componentPorts) {
44
+ export function computeAppProperties(launch, componentPorts, appUrl) {
45
+ if (appUrl !== undefined)
46
+ return suppliedAppProperties(launch.name, appUrl);
25
47
  let primaryPort = 0;
26
- for (const [name, component] of Object.entries(launch.components)) {
27
- // Only endpoints explicitly marked `exposed: true` are reachable from
28
- // outside the host (D-27), so only they can be the app's public address.
29
- // This provider has no orchestrator-facing publication channel (#294), so
30
- // $app.* always comes from its own routing strategy. The docker provider
31
- // answers by this same rule only when no orchestrator supplies an appUrl
32
- // (PROVIDERS.md §7).
33
- const hasExposed = component.provides?.some((p) => p.exposed === true) ?? false;
34
- if (hasExposed && componentPorts[name]) {
35
- primaryPort = componentPorts[name];
36
- break;
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
+ }
37
64
  }
38
65
  }
39
66
  const url = primaryPort > 0 ? `http://localhost:${primaryPort}` : "";
@@ -45,11 +72,38 @@ export function computeAppProperties(launch, componentPorts) {
45
72
  ...deriveAppUrlProperties(url),
46
73
  };
47
74
  }
75
+ /**
76
+ * `$app.endpoints.<name>.*` under this provider (D-63 rule 4): every
77
+ * property of every named published endpoint resolves `""`. The allocator
78
+ * hands out one port per **component** (`allocatePorts`, keyed by component
79
+ * name), so a second `exposed: true` entry on a component has no host-side
80
+ * address to publish — not the primary's, and not its own (#294). The
81
+ * primary's entry is `""` too, rather than a copy of `$app.*`, because the
82
+ * per-endpoint form promises a per-endpoint publication this provider does
83
+ * not perform; `$app.*` keeps its own routing answer. Registering the empty
84
+ * answer explicitly, rather than nothing, records that the provider has read
85
+ * the namespace and declined it.
86
+ */
87
+ export function computeAppEndpoints(launch) {
88
+ const endpoints = {};
89
+ for (const component of Object.values(launch.components)) {
90
+ for (const p of component.provides ?? []) {
91
+ if (p.name === undefined || p.exposed !== true)
92
+ continue;
93
+ endpoints[p.name] ??= UNPUBLISHED_APP_ENDPOINT;
94
+ }
95
+ }
96
+ return endpoints;
97
+ }
48
98
  /**
49
99
  * Build a ResolverContext from provisioned resources, component ports,
50
- * secrets, and (D-33) the platform-injected app properties.
100
+ * secrets, (D-33) the platform-injected app properties, (D-63) the
101
+ * per-endpoint map — `computeAppEndpoints`, which under this provider is
102
+ * every named published endpoint resolving `""` — and the `uses` each
103
+ * resource entry declares (`declaredUses`), which the resolver reads to
104
+ * resolve `$<resource>.<use>.<property>` strictly.
51
105
  */
52
- export function buildResolverContext(resourceMap, componentPorts, secrets, app) {
106
+ export function buildResolverContext(resourceMap, componentPorts, secrets, app, appEndpoints = {}, uses = {}) {
53
107
  // Build components map from ports
54
108
  const components = {};
55
109
  for (const [name, port] of Object.entries(componentPorts)) {
@@ -70,7 +124,78 @@ export function buildResolverContext(resourceMap, componentPorts, secrets, app)
70
124
  }
71
125
  resources[name] = record;
72
126
  }
73
- return { resources, components, secrets, app };
127
+ return { resources, components, secrets, app, appEndpoints, uses };
128
+ }
129
+ /**
130
+ * The use keys each resource entry declares (`db`, `db.cache`), keyed like
131
+ * the resource namespace (`name ?? type`, app-global). Same-name entries pool
132
+ * their keys — D-24 says they describe one resource. Host-capability entries
133
+ * have none.
134
+ */
135
+ export function declaredUses(launch) {
136
+ const uses = {};
137
+ for (const component of Object.values(launch.components)) {
138
+ for (const entry of [...(component.requires ?? []), ...(component.supports ?? [])]) {
139
+ if (entry.host || !entry.uses)
140
+ continue;
141
+ const pooled = (uses[entry.name ?? entry.type] ??= []);
142
+ for (const key of useKeys(entry.uses)) {
143
+ if (!pooled.includes(key))
144
+ pooled.push(key);
145
+ }
146
+ }
147
+ }
148
+ return uses;
149
+ }
150
+ /**
151
+ * A resource's property map as `up`, `env` and `bootstrap` all register it:
152
+ * the provisioner's instance vocabulary plus every pooled use this provider
153
+ * covers under `<use>.<property>` / `<use>.<name>.<property>` (D-24:
154
+ * same-name entries describe one resource). `dbIndexes` carries the numbered
155
+ * database each redis `db` use key selects. A pooled `db` key with no index
156
+ * in it stays unregistered, so a `$<resource>.db.<property>` (or
157
+ * `$<resource>.db.<name>.<property>`) reference throws `UnresolvedUseError`
158
+ * instead of falling through to the instance url.
159
+ */
160
+ export function registerResource(type, resourceName, uses, base, dbIndexes) {
161
+ const pooled = coveredUses(type, uses[resourceName] ?? []);
162
+ const registered = pooled.filter((key) => parseUseKey(key).use !== "db" || Object.hasOwn(dbIndexes, key));
163
+ return withCoveredUses(type, registered, base, dbIndexes);
164
+ }
165
+ /**
166
+ * The resource map a subcommand run after `up` (`env`, `bootstrap`) resolves
167
+ * against, rebuilt from state and registered exactly as `up` registered it.
168
+ * Each recorded resource is re-provisioned to read its current properties —
169
+ * every provisioner is idempotent, so this neither re-creates nor corrupts
170
+ * the resource — and a redis `db` use reads back the index `up` recorded
171
+ * rather than one re-derived from the file, which may have changed since. A
172
+ * state file written before the index was recorded leaves `db.*`
173
+ * unregistered, so the strict resolver throws for `$<resource>.db.*` — never
174
+ * the instance url — until the next `up` records it.
175
+ */
176
+ export async function resourceMapFromState(launch, state, projectDir) {
177
+ const uses = declaredUses(launch);
178
+ const resourceMap = {};
179
+ for (const [name, res] of Object.entries(state.resources)) {
180
+ const provisioner = getProvisioner(res.type);
181
+ if (!provisioner)
182
+ continue;
183
+ const result = await provisioner.provision({ type: res.type, name: res.name }, { appName: state.appName, projectDir, databases: namedDatabases(uses[name] ?? []) }, res);
184
+ resourceMap[name] = registerResource(res.type, name, uses, result.properties, recordedDbIndexes(res));
185
+ }
186
+ return resourceMap;
187
+ }
188
+ /**
189
+ * The resolver context `up`, `env` and `bootstrap` share, built from the same
190
+ * inputs: the registered resources, the recorded ports and secrets, `$app.*`
191
+ * from the recorded publication context (D-58) with a satisfied
192
+ * `https-origin` wired to the same string (D-60 rule 4), and the declared
193
+ * uses the resolver applies strictly.
194
+ */
195
+ export function resolverContextFor(launch, resourceMap, state) {
196
+ const appProperties = computeAppProperties(launch, state.ports, state.appUrl);
197
+ wireHttpsOrigins(launch, resourceMap, state.appUrl);
198
+ return buildResolverContext(resourceMap, state.ports, state.secrets, appProperties, computeAppEndpoints(launch), declaredUses(launch));
74
199
  }
75
200
  /**
76
201
  * Resolve all environment variables for a single component.
@@ -256,7 +381,7 @@ export async function writeAllEnvFiles(launch, context, resourceMap, componentPo
256
381
  }
257
382
  else {
258
383
  const envDir = join(projectDir, ".launchfile", "env");
259
- await mkdir(envDir, { recursive: true });
384
+ await mkdir(envDir, { recursive: true, mode: 0o700 });
260
385
  await writeEnvFile(join(envDir, `${name}.env`), env);
261
386
  }
262
387
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `https-origin` (D-60) under this provider — the one backing service that
3
+ * sits in FRONT of the app.
4
+ *
5
+ * This provider runs no edge of its own, so it cannot provision an origin. It
6
+ * can accept one the orchestrator already owns, through the publication-context
7
+ * channel (`LaunchUpOpts.appUrl`, D-58), which for this type IS the D-56
8
+ * supplied-resource channel (D-60 rule 5, PROVIDERS.md §7) — not a second one.
9
+ * Satisfaction is decided on the supplied scheme alone: no request is made, and
10
+ * D-56 rule 3 stands — the provider does not verify the origin exists or is
11
+ * ready.
12
+ */
13
+ import { type NormalizedLaunch, type NormalizedRequirement } from "@launchfile/sdk";
14
+ import type { ResourceProperties } from "./resources/types.js";
15
+ /** The backing-service type that declares the app's public HTTPS origin (D-60). */
16
+ export declare const HTTPS_ORIGIN = "https-origin";
17
+ /**
18
+ * Whether a supplied publication URL satisfies an `https-origin` entry: its
19
+ * scheme is `https`. Syntactic only — `undefined` (nothing supplied and nothing
20
+ * recorded) and an `http` URL both fail. Expects a normalized value, as
21
+ * `launchUp` records it.
22
+ */
23
+ export declare function httpsOriginSatisfied(appUrl: string | undefined): boolean;
24
+ /**
25
+ * Why one `https-origin` entry is not satisfied, for a refusal or a degraded
26
+ * note — the same two reasons `@launchfile/docker` gives, so one Launchfile
27
+ * behind one proxy reads the same message under either provider (P-5).
28
+ */
29
+ export declare function httpsOriginShortfall(entry: NormalizedRequirement, appUrl: string | undefined): string;
30
+ /**
31
+ * The component an `https-origin` entry sits on, when the file declares one
32
+ * (D-60 rule 3), else `undefined`. **Declaration** fixes the primary, not
33
+ * 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.
37
+ */
38
+ export declare function declaredPrimaryComponent(launch: NormalizedLaunch): string | undefined;
39
+ /**
40
+ * Register every satisfied `https-origin` entry as a resource so its `set_env`
41
+ * 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.
46
+ */
47
+ export declare function wireHttpsOrigins(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, appUrl: string | undefined): void;
48
+ //# sourceMappingURL=https-origin.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `https-origin` (D-60) under this provider — the one backing service that
3
+ * sits in FRONT of the app.
4
+ *
5
+ * This provider runs no edge of its own, so it cannot provision an origin. It
6
+ * can accept one the orchestrator already owns, through the publication-context
7
+ * channel (`LaunchUpOpts.appUrl`, D-58), which for this type IS the D-56
8
+ * supplied-resource channel (D-60 rule 5, PROVIDERS.md §7) — not a second one.
9
+ * Satisfaction is decided on the supplied scheme alone: no request is made, and
10
+ * D-56 rule 3 stands — the provider does not verify the origin exists or is
11
+ * ready.
12
+ */
13
+ import { suppliedAppAddress, } from "@launchfile/sdk";
14
+ /** The backing-service type that declares the app's public HTTPS origin (D-60). */
15
+ export const HTTPS_ORIGIN = "https-origin";
16
+ /**
17
+ * Whether a supplied publication URL satisfies an `https-origin` entry: its
18
+ * scheme is `https`. Syntactic only — `undefined` (nothing supplied and nothing
19
+ * recorded) and an `http` URL both fail. Expects a normalized value, as
20
+ * `launchUp` records it.
21
+ */
22
+ export function httpsOriginSatisfied(appUrl) {
23
+ return appUrl !== undefined && suppliedAppAddress(appUrl).scheme === "https";
24
+ }
25
+ /**
26
+ * Why one `https-origin` entry is not satisfied, for a refusal or a degraded
27
+ * note — the same two reasons `@launchfile/docker` gives, so one Launchfile
28
+ * behind one proxy reads the same message under either provider (P-5).
29
+ */
30
+ export function httpsOriginShortfall(entry, appUrl) {
31
+ const label = `${entry.name ?? entry.type} (endpoint "${entry.endpoint ?? "?"}")`;
32
+ return appUrl === undefined
33
+ ? `${label}: no publication URL was supplied, and this provider has no edge of its own`
34
+ : `${label}: the supplied publication URL's scheme is "${suppliedAppAddress(appUrl).scheme}", not https`;
35
+ }
36
+ /**
37
+ * The component an `https-origin` entry sits on, when the file declares one
38
+ * (D-60 rule 3), else `undefined`. **Declaration** fixes the primary, not
39
+ * fulfillment: a `supports:` entry this provider leaves unsatisfied still
40
+ * names it, so `$app.*` does not change value with the provider's capability.
41
+ * The SDK caps the app at one such entry and requires it to sit on the
42
+ * component that owns the named endpoint, so the first match is the only one.
43
+ */
44
+ export function declaredPrimaryComponent(launch) {
45
+ for (const [name, component] of Object.entries(launch.components)) {
46
+ for (const entry of [
47
+ ...(component.requires ?? []),
48
+ ...(component.supports ?? []),
49
+ ]) {
50
+ if (entry.type === HTTPS_ORIGIN && entry.endpoint !== undefined)
51
+ return name;
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Register every satisfied `https-origin` entry as a resource so its `set_env`
58
+ * resolves. One registered property, `url` (D-60 rule 4), holding the same
59
+ * string as `$app.url`. Mutates `resourceMap`; a no-op when the recorded
60
+ * publication URL does not satisfy the type, so an unsatisfied entry's
61
+ * `set_env` stays absent (never `""`) exactly as for any other resource this
62
+ * provider did not provision.
63
+ */
64
+ export function wireHttpsOrigins(launch, resourceMap, appUrl) {
65
+ if (appUrl === undefined)
66
+ return;
67
+ const { scheme, url } = suppliedAppAddress(appUrl);
68
+ if (scheme !== "https")
69
+ return;
70
+ for (const component of Object.values(launch.components)) {
71
+ for (const entry of [
72
+ ...(component.requires ?? []),
73
+ ...(component.supports ?? []),
74
+ ]) {
75
+ if (entry.type !== HTTPS_ORIGIN)
76
+ continue;
77
+ resourceMap[entry.name ?? entry.type] = { url };
78
+ }
79
+ }
80
+ }
81
+ //# sourceMappingURL=https-origin.js.map
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@
7
7
  export { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
8
8
  export type { LaunchUpOpts } from "./provider.js";
9
9
  export { launchBootstrap } from "./bootstrap.js";
10
+ export { InvalidAppUrlError, normalizeAppUrl } from "@launchfile/sdk";
10
11
  export type { BootstrapResult } from "./bootstrap.js";
11
12
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -6,4 +6,8 @@
6
6
  */
7
7
  export { launchUp, launchDown, launchStatus, launchEnv } from "./provider.js";
8
8
  export { launchBootstrap } from "./bootstrap.js";
9
+ // Re-exported so a caller of this provider catches the publication-URL refusal
10
+ // (D-58 rule 3) without also depending on the SDK, matching
11
+ // `@launchfile/docker`. The SDK owns both.
12
+ export { InvalidAppUrlError, normalizeAppUrl } from "@launchfile/sdk";
9
13
  //# sourceMappingURL=index.js.map
@@ -16,6 +16,7 @@ export declare function allocatePort(key: string, existingPorts: Set<number>): P
16
16
  export declare function allocatePorts(components: Record<string, {
17
17
  provides?: Array<{
18
18
  port: number;
19
+ exposed?: boolean;
19
20
  }>;
20
21
  }>, appName: string, savedPorts?: Record<string, number>): Promise<Record<string, number>>;
21
22
  //# sourceMappingURL=port-allocator.d.ts.map
@@ -61,8 +61,22 @@ export async function allocatePorts(components, appName, savedPorts) {
61
61
  allocated.add(saved);
62
62
  continue;
63
63
  }
64
- // Use the component's declared port if free
65
- const declaredPort = component.provides?.[0]?.port;
64
+ // Use the component's declared port if free.
65
+ //
66
+ // D-27: only an `exposed: true` endpoint is reachable from outside the
67
+ // host, so it anchors the single port this provider allocates. Fully
68
+ // internal components keep provides[0]. The docker provider answers
69
+ // $app.* by this same rule (app-url.ts), and this line is what makes the
70
+ // two agree on a file whose first entry is not the exposed one.
71
+ //
72
+ // Residual: this provider runs one host process per component and
73
+ // allocates it one port, so a component declaring several endpoints
74
+ // collapses to the anchor — every non-anchor endpoint is unallocated
75
+ // here. The allocated port also need not equal any declared port: when
76
+ // the preferred one is taken, the fall-through below picks a
77
+ // deterministic free port instead. See #276 for what that costs
78
+ // `$components.<name>.<endpoint>.*` on this provider.
79
+ const declaredPort = (component.provides?.find((p) => p.exposed === true) ?? component.provides?.[0])?.port;
66
80
  if (declaredPort && !allocated.has(declaredPort) && (await isPortFree(declaredPort))) {
67
81
  result[name] = declaredPort;
68
82
  allocated.add(declaredPort);