@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 +8 -19
- package/dist/env-writer.d.ts +87 -9
- package/dist/env-writer.js +172 -22
- package/dist/https-origin.d.ts +48 -0
- package/dist/https-origin.js +81 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/port-allocator.d.ts +1 -0
- package/dist/port-allocator.js +16 -2
- package/dist/process-stopper.d.ts +8 -4
- package/dist/process-stopper.js +19 -11
- package/dist/provider.d.ts +69 -7
- package/dist/provider.js +232 -56
- package/dist/resources/index.d.ts +2 -1
- package/dist/resources/index.js +1 -0
- package/dist/resources/mysql.d.ts +2 -2
- package/dist/resources/mysql.js +23 -3
- package/dist/resources/postgres.d.ts +2 -2
- package/dist/resources/postgres.js +35 -9
- package/dist/resources/redis.d.ts +2 -2
- package/dist/resources/redis.js +1 -1
- package/dist/resources/sqlite.d.ts +2 -2
- package/dist/resources/sqlite.js +26 -5
- package/dist/resources/types.d.ts +18 -1
- package/dist/resources/uses.d.ts +107 -0
- package/dist/resources/uses.js +217 -0
- package/dist/state.d.ts +36 -0
- package/dist/state.js +36 -3
- package/package.json +2 -2
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 {
|
|
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
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
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 = [];
|
package/dist/env-writer.d.ts
CHANGED
|
@@ -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.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
*/
|
package/dist/env-writer.js
CHANGED
|
@@ -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.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|
|
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
|
package/dist/port-allocator.d.ts
CHANGED
|
@@ -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
|
package/dist/port-allocator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
/**
|