@launchfile/macos-dev 0.9.0 → 0.11.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.js CHANGED
@@ -18,9 +18,8 @@ import { join } from "node:path";
18
18
  import { spawn } from "node:child_process";
19
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";
21
+ import { resolveComponentEnv, resolveGenerators, resolverContextFor, resourceMapFromState, } from "./env-writer.js";
22
22
  import { redactSecrets, registerDeclaredSecret } from "./redact.js";
23
- import { getProvisioner } from "./resources/index.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
  /**
@@ -191,23 +190,13 @@ export async function launchBootstrap(opts = {}) {
191
190
  if (!state) {
192
191
  throw new Error("No active launch state. Run `launch up` first.");
193
192
  }
194
- // Rebuild resource map from state so the resolver context has real
195
- // values at invocation time (same pattern as launchEnv). This calls
196
- // provisioner.provision() on already-provisioned resources to retrieve
197
- // their current property values — relies on every provisioner being
198
- // idempotent: re-running provision() must not corrupt state or
199
- // re-create the resource. All current provisioners satisfy this; new
200
- // provisioners must as well.
201
- const resourceMap = {};
202
- for (const [name, res] of Object.entries(state.resources)) {
203
- const provisioner = getProvisioner(res.type);
204
- if (provisioner) {
205
- const result = await provisioner.provision({ type: res.type, name: res.name }, { appName: state.appName, projectDir }, res);
206
- resourceMap[name] = result.properties;
207
- }
208
- }
209
- const appProperties = computeAppProperties(launch, state.ports);
210
- 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);
211
200
  const exec = opts.exec ?? defaultExec;
212
201
  const plan = planBootstraps(launch, context, { component: opts.component });
213
202
  const results = [];
@@ -4,28 +4,106 @@
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
+ * `declared` carries the declared components, whose `provides` entries are
64
+ * what the named-endpoint form `$components.<name>.<endpoint>.<prop>`
65
+ * (D-6, D-66) resolves against. Omit it and only the primary
66
+ * `url`/`host`/`port` are registered.
67
+ */
68
+ 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[]>, declared?: Record<string, NormalizedComponent>): ResolverContext;
69
+ /**
70
+ * The use keys each resource entry declares (`db`, `db.cache`), keyed like
71
+ * the resource namespace (`name ?? type`, app-global). Same-name entries pool
72
+ * their keys — D-24 says they describe one resource. Host-capability entries
73
+ * have none.
74
+ */
75
+ export declare function declaredUses(launch: NormalizedLaunch): Record<string, string[]>;
76
+ /**
77
+ * A resource's property map as `up`, `env` and `bootstrap` all register it:
78
+ * the provisioner's instance vocabulary plus every pooled use this provider
79
+ * covers under `<use>.<property>` / `<use>.<name>.<property>` (D-24:
80
+ * same-name entries describe one resource). `dbIndexes` carries the numbered
81
+ * database each redis `db` use key selects. A pooled `db` key with no index
82
+ * in it stays unregistered, so a `$<resource>.db.<property>` (or
83
+ * `$<resource>.db.<name>.<property>`) reference throws `UnresolvedUseError`
84
+ * instead of falling through to the instance url.
85
+ */
86
+ export declare function registerResource(type: string, resourceName: string, uses: Record<string, readonly string[]>, base: ResourceProperties, dbIndexes: DbIndexes): ResourceProperties;
87
+ /**
88
+ * The resource map a subcommand run after `up` (`env`, `bootstrap`) resolves
89
+ * against, rebuilt from state and registered exactly as `up` registered it.
90
+ * Each recorded resource is re-provisioned to read its current properties —
91
+ * every provisioner is idempotent, so this neither re-creates nor corrupts
92
+ * the resource — and a redis `db` use reads back the index `up` recorded
93
+ * rather than one re-derived from the file, which may have changed since. A
94
+ * state file written before the index was recorded leaves `db.*`
95
+ * unregistered, so the strict resolver throws for `$<resource>.db.*` — never
96
+ * the instance url — until the next `up` records it.
97
+ */
98
+ export declare function resourceMapFromState(launch: NormalizedLaunch, state: LaunchState, projectDir: string): Promise<Record<string, ResourceProperties>>;
99
+ /**
100
+ * The resolver context `up`, `env` and `bootstrap` share, built from the same
101
+ * inputs: the registered resources, the recorded ports and secrets, `$app.*`
102
+ * from the recorded publication context (D-58) with a satisfied
103
+ * `https-origin` wired to the same string (D-60 rule 4), and the declared
104
+ * uses the resolver applies strictly.
27
105
  */
28
- export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>): ResolverContext;
106
+ export declare function resolverContextFor(launch: NormalizedLaunch, resourceMap: Record<string, ResourceProperties>, state: LaunchState): ResolverContext;
29
107
  /**
30
108
  * The resolved environment for one component, plus what the file did not supply.
31
109
  */
@@ -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, endpointProperties, 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,62 @@ 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
+ }
98
+ /**
99
+ * The `provides` entries this provider can name a reachable port for, or
100
+ * `undefined` when it can name none.
101
+ *
102
+ * macos-dev allocates exactly one host port per component and hands it to the
103
+ * process as `PORT`. That port belongs to whichever declared endpoint the
104
+ * allocator anchored on; the rest are unallocated, and the process binds the
105
+ * ports they declare. When the component's preferred port was taken, the
106
+ * allocator moves it to a free port outside the declared set — then no declared
107
+ * endpoint can be named at that port, so the component reports no per-endpoint
108
+ * properties rather than an endpoint address nothing listens on (L-4). A
109
+ * container provider has no such collapse: it binds every declared port.
110
+ */
111
+ function allocatedEndpoints(component, allocatedPort) {
112
+ const provides = component?.provides;
113
+ if (!provides?.some((entry) => entry.port === allocatedPort))
114
+ return undefined;
115
+ return provides;
116
+ }
48
117
  /**
49
118
  * Build a ResolverContext from provisioned resources, component ports,
50
- * secrets, and (D-33) the platform-injected app properties.
119
+ * secrets, (D-33) the platform-injected app properties, (D-63) the
120
+ * per-endpoint map — `computeAppEndpoints`, which under this provider is
121
+ * every named published endpoint resolving `""` — and the `uses` each
122
+ * resource entry declares (`declaredUses`), which the resolver reads to
123
+ * resolve `$<resource>.<use>.<property>` strictly.
124
+ *
125
+ * `declared` carries the declared components, whose `provides` entries are
126
+ * what the named-endpoint form `$components.<name>.<endpoint>.<prop>`
127
+ * (D-6, D-66) resolves against. Omit it and only the primary
128
+ * `url`/`host`/`port` are registered.
51
129
  */
52
- export function buildResolverContext(resourceMap, componentPorts, secrets, app) {
130
+ export function buildResolverContext(resourceMap, componentPorts, secrets, app, appEndpoints = {}, uses = {}, declared) {
53
131
  // Build components map from ports
54
132
  const components = {};
55
133
  for (const [name, port] of Object.entries(componentPorts)) {
@@ -57,6 +135,7 @@ export function buildResolverContext(resourceMap, componentPorts, secrets, app)
57
135
  url: `http://localhost:${port}`,
58
136
  host: "localhost",
59
137
  port,
138
+ ...endpointProperties(allocatedEndpoints(declared?.[name], port), "localhost"),
60
139
  };
61
140
  }
62
141
  // Build named resources map
@@ -70,7 +149,78 @@ export function buildResolverContext(resourceMap, componentPorts, secrets, app)
70
149
  }
71
150
  resources[name] = record;
72
151
  }
73
- return { resources, components, secrets, app };
152
+ return { resources, components, secrets, app, appEndpoints, uses };
153
+ }
154
+ /**
155
+ * The use keys each resource entry declares (`db`, `db.cache`), keyed like
156
+ * the resource namespace (`name ?? type`, app-global). Same-name entries pool
157
+ * their keys — D-24 says they describe one resource. Host-capability entries
158
+ * have none.
159
+ */
160
+ export function declaredUses(launch) {
161
+ const uses = {};
162
+ for (const component of Object.values(launch.components)) {
163
+ for (const entry of [...(component.requires ?? []), ...(component.supports ?? [])]) {
164
+ if (entry.host || !entry.uses)
165
+ continue;
166
+ const pooled = (uses[entry.name ?? entry.type] ??= []);
167
+ for (const key of useKeys(entry.uses)) {
168
+ if (!pooled.includes(key))
169
+ pooled.push(key);
170
+ }
171
+ }
172
+ }
173
+ return uses;
174
+ }
175
+ /**
176
+ * A resource's property map as `up`, `env` and `bootstrap` all register it:
177
+ * the provisioner's instance vocabulary plus every pooled use this provider
178
+ * covers under `<use>.<property>` / `<use>.<name>.<property>` (D-24:
179
+ * same-name entries describe one resource). `dbIndexes` carries the numbered
180
+ * database each redis `db` use key selects. A pooled `db` key with no index
181
+ * in it stays unregistered, so a `$<resource>.db.<property>` (or
182
+ * `$<resource>.db.<name>.<property>`) reference throws `UnresolvedUseError`
183
+ * instead of falling through to the instance url.
184
+ */
185
+ export function registerResource(type, resourceName, uses, base, dbIndexes) {
186
+ const pooled = coveredUses(type, uses[resourceName] ?? []);
187
+ const registered = pooled.filter((key) => parseUseKey(key).use !== "db" || Object.hasOwn(dbIndexes, key));
188
+ return withCoveredUses(type, registered, base, dbIndexes);
189
+ }
190
+ /**
191
+ * The resource map a subcommand run after `up` (`env`, `bootstrap`) resolves
192
+ * against, rebuilt from state and registered exactly as `up` registered it.
193
+ * Each recorded resource is re-provisioned to read its current properties —
194
+ * every provisioner is idempotent, so this neither re-creates nor corrupts
195
+ * the resource — and a redis `db` use reads back the index `up` recorded
196
+ * rather than one re-derived from the file, which may have changed since. A
197
+ * state file written before the index was recorded leaves `db.*`
198
+ * unregistered, so the strict resolver throws for `$<resource>.db.*` — never
199
+ * the instance url — until the next `up` records it.
200
+ */
201
+ export async function resourceMapFromState(launch, state, projectDir) {
202
+ const uses = declaredUses(launch);
203
+ const resourceMap = {};
204
+ for (const [name, res] of Object.entries(state.resources)) {
205
+ const provisioner = getProvisioner(res.type);
206
+ if (!provisioner)
207
+ continue;
208
+ const result = await provisioner.provision({ type: res.type, name: res.name }, { appName: state.appName, projectDir, databases: namedDatabases(uses[name] ?? []) }, res);
209
+ resourceMap[name] = registerResource(res.type, name, uses, result.properties, recordedDbIndexes(res));
210
+ }
211
+ return resourceMap;
212
+ }
213
+ /**
214
+ * The resolver context `up`, `env` and `bootstrap` share, built from the same
215
+ * inputs: the registered resources, the recorded ports and secrets, `$app.*`
216
+ * from the recorded publication context (D-58) with a satisfied
217
+ * `https-origin` wired to the same string (D-60 rule 4), and the declared
218
+ * uses the resolver applies strictly.
219
+ */
220
+ export function resolverContextFor(launch, resourceMap, state) {
221
+ const appProperties = computeAppProperties(launch, state.ports, state.appUrl);
222
+ wireHttpsOrigins(launch, resourceMap, state.appUrl);
223
+ return buildResolverContext(resourceMap, state.ports, state.secrets, appProperties, computeAppEndpoints(launch), declaredUses(launch), launch.components);
74
224
  }
75
225
  /**
76
226
  * Resolve all environment variables for a single component.
@@ -256,7 +406,7 @@ export async function writeAllEnvFiles(launch, context, resourceMap, componentPo
256
406
  }
257
407
  else {
258
408
  const envDir = join(projectDir, ".launchfile", "env");
259
- await mkdir(envDir, { recursive: true });
409
+ await mkdir(envDir, { recursive: true, mode: 0o700 });
260
410
  await writeEnvFile(join(envDir, `${name}.env`), env);
261
411
  }
262
412
  }
@@ -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);
@@ -16,9 +16,12 @@
16
16
  * 1. **Liveness** — `process.kill(pid, 0)` throws ESRCH if no such process
17
17
  * exists. If it's dead, we skip (already stopped).
18
18
  * 2. **Identity** — we compare the recorded spawn time against the live
19
- * process's actual start time via `ps -o lstart= -p <pid>`. If the live
20
- * process started meaningfully later than we recorded, the pid was
21
- * recycled and we REFUSE to signal it.
19
+ * process's actual start time via `ps -o lstart= -p <pid>`. If the two
20
+ * are further apart than the tolerance window, in either direction, the
21
+ * record does not describe this process and we REFUSE to signal it. A
22
+ * later live start means the pid was recycled; an earlier one means the
23
+ * record itself is untrustworthy (a backward clock jump between spawn and
24
+ * `down`, or a state file carried over from another run).
22
25
  *
23
26
  * This is a best-effort guarantee, not a cryptographic one. Its honest limits:
24
27
  * - `ps lstart` has ~1s resolution, so we allow a small tolerance window. A
@@ -76,7 +79,8 @@ export declare const realSignalFns: SignalFns;
76
79
  * - "alive-verified": process exists AND start time is consistent → safe to signal
77
80
  * - "alive-unverified": process exists but start time couldn't be read → signal group only, cautiously
78
81
  * - "dead": no such process (ESRCH) → already stopped, skip
79
- * - "mismatch": process exists but started too late → recycled pid, DO NOT signal
82
+ * - "mismatch": process exists but its start time is outside the tolerance
83
+ * window on either side → the record does not describe it, DO NOT signal
80
84
  */
81
85
  export declare function checkIdentity(rec: RecordedProcess, fns: SignalFns): Promise<"alive-verified" | "alive-unverified" | "dead" | "mismatch">;
82
86
  /**