@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.
- package/dist/bootstrap.d.ts +7 -0
- package/dist/bootstrap.js +25 -30
- package/dist/env-writer.d.ts +82 -9
- package/dist/env-writer.js +147 -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/provider.d.ts +69 -7
- package/dist/provider.js +231 -55
- 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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { shell, shellOk } from "../shell.js";
|
|
11
11
|
import { generatePassword } from "../secret-generator.js";
|
|
12
12
|
import { assertSafeIdentifier, assertSafePassword, SAFE_IDENTIFIER, } from "./identifiers.js";
|
|
13
|
+
import { namedDatabase } from "./uses.js";
|
|
13
14
|
const DEFAULT_PORT = 5432;
|
|
14
15
|
const DEFAULT_HOST = "localhost";
|
|
15
16
|
const READY_TIMEOUT_SECONDS = 10;
|
|
@@ -28,6 +29,20 @@ export class PostgresProvisioner {
|
|
|
28
29
|
async isRunning() {
|
|
29
30
|
return this.#shellOk("pg_isready", ["-q"]);
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether `database` exists on the server. psql exits 0 whenever the query
|
|
34
|
+
* ran, a zero-row SELECT included, so the exit code says nothing about
|
|
35
|
+
* existence — only the `1` the row prints does. The caller validates the
|
|
36
|
+
* name before it reaches the SQL here.
|
|
37
|
+
*/
|
|
38
|
+
async #databaseExists(port, database) {
|
|
39
|
+
const result = await this.#shell("psql", [
|
|
40
|
+
...psqlArgs(port, "postgres"),
|
|
41
|
+
"-tAc",
|
|
42
|
+
`SELECT 1 FROM pg_database WHERE datname='${database}'`,
|
|
43
|
+
], { allowFailure: true, silent: true });
|
|
44
|
+
return result.exitCode === 0 && result.stdout.trim() === "1";
|
|
45
|
+
}
|
|
31
46
|
async provision(req, opts, existingState) {
|
|
32
47
|
// Ensure postgres is running
|
|
33
48
|
if (!(await this.isRunning())) {
|
|
@@ -77,12 +92,7 @@ export class PostgresProvisioner {
|
|
|
77
92
|
// silent: this command embeds the generated DB password; don't echo it (CWE-532).
|
|
78
93
|
{ allowFailure: true, silent: true });
|
|
79
94
|
// Create database (idempotent)
|
|
80
|
-
|
|
81
|
-
...psqlArgs(port, "postgres"),
|
|
82
|
-
"-tAc",
|
|
83
|
-
`SELECT 1 FROM pg_database WHERE datname='${dbName}'`,
|
|
84
|
-
]);
|
|
85
|
-
if (!dbExists) {
|
|
95
|
+
if (!(await this.#databaseExists(port, dbName))) {
|
|
86
96
|
await this.#shell("createdb", ["-h", DEFAULT_HOST, "-p", String(port), "-O", user, dbName], { allowFailure: true });
|
|
87
97
|
}
|
|
88
98
|
// Handle extensions from config
|
|
@@ -104,6 +114,19 @@ export class PostgresProvisioner {
|
|
|
104
114
|
], { allowFailure: true });
|
|
105
115
|
}
|
|
106
116
|
}
|
|
117
|
+
// Named `database` uses (SPEC.md § Resource uses): one more database per
|
|
118
|
+
// name, `<instance>_<name>`, created the same way as the app's own.
|
|
119
|
+
// Security: a use name is schema-validated (^[a-z][a-z0-9-]*$) and the
|
|
120
|
+
// hyphens become underscores, so the identifier check cannot fail on a
|
|
121
|
+
// name that reached here through the parser; it guards the SQL below
|
|
122
|
+
// all the same.
|
|
123
|
+
const databases = (opts.databases ?? []).map((name) => namedDatabase(dbName, name));
|
|
124
|
+
for (const database of databases) {
|
|
125
|
+
assertSafeIdentifier(database, "database name");
|
|
126
|
+
if (!(await this.#databaseExists(port, database))) {
|
|
127
|
+
await this.#shell("createdb", ["-h", DEFAULT_HOST, "-p", String(port), "-O", user, database], { allowFailure: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
107
130
|
const url = `postgresql://${user}:${password}@${DEFAULT_HOST}:${port}/${dbName}`;
|
|
108
131
|
const properties = {
|
|
109
132
|
url,
|
|
@@ -121,13 +144,16 @@ export class PostgresProvisioner {
|
|
|
121
144
|
dbName,
|
|
122
145
|
user,
|
|
123
146
|
password,
|
|
147
|
+
...(databases.length > 0 ? { databases } : {}),
|
|
124
148
|
};
|
|
125
149
|
return { properties, state };
|
|
126
150
|
}
|
|
127
|
-
async destroy(state) {
|
|
151
|
+
async destroy(state, _opts) {
|
|
128
152
|
// Security: state values come from disk (state.json) — validate before SQL interpolation
|
|
129
|
-
|
|
130
|
-
|
|
153
|
+
for (const database of [state.dbName, ...(state.databases ?? [])]) {
|
|
154
|
+
if (!database || !SAFE_IDENTIFIER.test(database))
|
|
155
|
+
continue;
|
|
156
|
+
await this.#shell("dropdb", ["-h", DEFAULT_HOST, "--if-exists", database], { allowFailure: true });
|
|
131
157
|
}
|
|
132
158
|
if (state.user && SAFE_IDENTIFIER.test(state.user)) {
|
|
133
159
|
await this.#shell("psql", [
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
5
5
|
import type { ResourceState } from "../state.js";
|
|
6
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
6
|
+
import type { DestroyOpts, ProvisionOpts, ResourceProperties, ResourceProvisioner, ShellRunner } from "./types.js";
|
|
7
7
|
export declare class RedisProvisioner implements ResourceProvisioner {
|
|
8
8
|
#private;
|
|
9
9
|
readonly type = "redis";
|
|
@@ -13,6 +13,6 @@ export declare class RedisProvisioner implements ResourceProvisioner {
|
|
|
13
13
|
properties: ResourceProperties;
|
|
14
14
|
state: ResourceState;
|
|
15
15
|
}>;
|
|
16
|
-
destroy(_state: ResourceState): Promise<void>;
|
|
16
|
+
destroy(_state: ResourceState, _opts: DestroyOpts): Promise<void>;
|
|
17
17
|
}
|
|
18
18
|
//# sourceMappingURL=redis.d.ts.map
|
package/dist/resources/redis.js
CHANGED
|
@@ -44,7 +44,7 @@ export class RedisProvisioner {
|
|
|
44
44
|
};
|
|
45
45
|
return { properties, state };
|
|
46
46
|
}
|
|
47
|
-
async destroy(_state) {
|
|
47
|
+
async destroy(_state, _opts) {
|
|
48
48
|
// Redis is shared, don't stop the service
|
|
49
49
|
// Could flush a specific database prefix, but not worth the complexity
|
|
50
50
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { NormalizedRequirement } from "@launchfile/sdk";
|
|
5
5
|
import type { ResourceState } from "../state.js";
|
|
6
|
-
import type { ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
6
|
+
import type { DestroyOpts, ProvisionOpts, ResourceProperties, ResourceProvisioner } from "./types.js";
|
|
7
7
|
export declare class SqliteProvisioner implements ResourceProvisioner {
|
|
8
8
|
readonly type = "sqlite";
|
|
9
9
|
isRunning(): Promise<boolean>;
|
|
@@ -11,6 +11,6 @@ export declare class SqliteProvisioner implements ResourceProvisioner {
|
|
|
11
11
|
properties: ResourceProperties;
|
|
12
12
|
state: ResourceState;
|
|
13
13
|
}>;
|
|
14
|
-
destroy(state: ResourceState): Promise<void>;
|
|
14
|
+
destroy(state: ResourceState, opts: DestroyOpts): Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
//# sourceMappingURL=sqlite.d.ts.map
|
package/dist/resources/sqlite.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SQLite resource provisioner — just creates a directory for the DB file.
|
|
3
3
|
*/
|
|
4
4
|
import { mkdir } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
6
|
export class SqliteProvisioner {
|
|
7
7
|
type = "sqlite";
|
|
8
8
|
async isRunning() {
|
|
@@ -28,11 +28,32 @@ export class SqliteProvisioner {
|
|
|
28
28
|
};
|
|
29
29
|
return { properties, state };
|
|
30
30
|
}
|
|
31
|
-
async destroy(state) {
|
|
32
|
-
if (state.dbName)
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
async destroy(state, opts) {
|
|
32
|
+
if (!state.dbName)
|
|
33
|
+
return;
|
|
34
|
+
// state.json is repo-supplied and parsed without validation (state.ts),
|
|
35
|
+
// so dbName is untrusted here. The repo also supplies .launchfile/, so
|
|
36
|
+
// the data directory itself can be a symlink: resolve() is lexical and
|
|
37
|
+
// would not see it, while fs.rm follows symlinked components at the OS
|
|
38
|
+
// layer. Anchor on realpath(projectDir), which the caller supplies, and
|
|
39
|
+
// compare the target's real parent against it.
|
|
40
|
+
// The trailing separator matters: a bare startsWith(root) would leave a
|
|
41
|
+
// sibling directory named `sqlite-evil` deletable.
|
|
42
|
+
const { realpath, rm } = await import("node:fs/promises");
|
|
43
|
+
const projectRoot = await realpath(opts.projectDir).catch(() => null);
|
|
44
|
+
if (projectRoot === null)
|
|
45
|
+
return;
|
|
46
|
+
const root = join(projectRoot, ".launchfile", "data", "sqlite");
|
|
47
|
+
const target = resolve(projectRoot, state.dbName);
|
|
48
|
+
const parent = await realpath(dirname(target)).catch(() => null);
|
|
49
|
+
if (parent === null)
|
|
50
|
+
return; // nothing at that path to delete
|
|
51
|
+
const real = join(parent, basename(target));
|
|
52
|
+
if (!real.startsWith(root + sep)) {
|
|
53
|
+
console.warn(` ! sqlite: refusing to delete ${target} — outside ${root}`);
|
|
54
|
+
return;
|
|
35
55
|
}
|
|
56
|
+
await rm(real, { force: true });
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
//# sourceMappingURL=sqlite.js.map
|
|
@@ -42,6 +42,23 @@ export interface ShellRunner {
|
|
|
42
42
|
export interface ProvisionOpts {
|
|
43
43
|
appName: string;
|
|
44
44
|
projectDir: string;
|
|
45
|
+
/**
|
|
46
|
+
* The names of the entry's named `database` uses (SPEC.md § Resource
|
|
47
|
+
* uses), pooled across same-name entries. A SQL provisioner creates one
|
|
48
|
+
* more database per name beside the app's — `<instance>_<name>` — and
|
|
49
|
+
* records them in state so `destroy` drops them. Other provisioners
|
|
50
|
+
* ignore it.
|
|
51
|
+
*/
|
|
52
|
+
databases?: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The trusted context a teardown runs in. `state.json` is repo-supplied and
|
|
56
|
+
* parsed without validation (`state.ts`), so a provisioner cannot treat a
|
|
57
|
+
* stored path or identifier as its own; `projectDir` comes from the caller and
|
|
58
|
+
* is the only trustworthy boundary a destroy can confine itself to.
|
|
59
|
+
*/
|
|
60
|
+
export interface DestroyOpts {
|
|
61
|
+
projectDir: string;
|
|
45
62
|
}
|
|
46
63
|
export interface ResourceProvisioner {
|
|
47
64
|
readonly type: string;
|
|
@@ -53,6 +70,6 @@ export interface ResourceProvisioner {
|
|
|
53
70
|
state: ResourceState;
|
|
54
71
|
}>;
|
|
55
72
|
/** Drop app-specific databases/users (destroy mode) */
|
|
56
|
-
destroy(state: ResourceState): Promise<void>;
|
|
73
|
+
destroy(state: ResourceState, opts: DestroyOpts): Promise<void>;
|
|
57
74
|
}
|
|
58
75
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of a `requires`/`supports` entry's declared `uses` (SPEC.md
|
|
3
|
+
* § Resource uses) by this provider's provisioners.
|
|
4
|
+
*
|
|
5
|
+
* A declared use is covered when the provisioner can hand over what the use
|
|
6
|
+
* asks for and register its properties under `<use>.<property>` — the dotted
|
|
7
|
+
* keys `$<resource>.<use>.<property>` resolves from. A named occurrence of a
|
|
8
|
+
* repeatable use (`- db: cache`) registers under `<use>.<name>.<property>`
|
|
9
|
+
* and is addressed as `$<resource>.<use>.<name>.<property>`. A use this
|
|
10
|
+
* provider cannot cover, a token it does not recognise, or a name on a use
|
|
11
|
+
* that does not repeat, refuses the component (PROVIDERS.md §10 item 5,
|
|
12
|
+
* D-64): no provider can claim to cover a use it does not know. This
|
|
13
|
+
* provider has no supplied-resource channel, so provision-and-cover or
|
|
14
|
+
* refuse are its only outcomes.
|
|
15
|
+
*
|
|
16
|
+
* Every function here takes **use keys** — `db` for a bare use, `db.cache`
|
|
17
|
+
* for a named one — the spelling `useKeys` from `@launchfile/sdk` produces
|
|
18
|
+
* and the resolver context lists.
|
|
19
|
+
*
|
|
20
|
+
* What each provisioner gives:
|
|
21
|
+
*
|
|
22
|
+
* - **redis** — the machine's Homebrew Redis, shared by every app on it. `db`
|
|
23
|
+
* is one numbered database on that server, allocated per use key within
|
|
24
|
+
* the app by {@link allocateDbIndexes}; `pubsub` and `server` are covered by
|
|
25
|
+
* the server itself and register nothing beyond the instance vocabulary.
|
|
26
|
+
* The server is shared across apps on this machine exactly as it is for an
|
|
27
|
+
* entry with no `uses` — a machine-wide index allocation is not attempted
|
|
28
|
+
* here.
|
|
29
|
+
* - **postgres / mysql / mariadb** — a per-app database on the local server.
|
|
30
|
+
* A bare `database` is that database; each named `database` is one more
|
|
31
|
+
* database on the server, `<instance>_<name>` ({@link namedDatabase}),
|
|
32
|
+
* created by the provisioner's own create-database path; `server` is the
|
|
33
|
+
* local instance.
|
|
34
|
+
*/
|
|
35
|
+
import { type NormalizedLaunch } from "@launchfile/sdk";
|
|
36
|
+
import type { ResourceProperties } from "./types.js";
|
|
37
|
+
/**
|
|
38
|
+
* The redis database index allocated to each `db` use key of one resource —
|
|
39
|
+
* `db` for the bare use, `db.<name>` for a named one. Produced per resource
|
|
40
|
+
* by {@link allocateDbIndexes}.
|
|
41
|
+
*/
|
|
42
|
+
export type DbIndexes = Readonly<Record<string, number>>;
|
|
43
|
+
/**
|
|
44
|
+
* The database a named `database` use gets on the local server:
|
|
45
|
+
* `<instance database>_<name>`, hyphens as underscores so the name is one SQL
|
|
46
|
+
* identifier on every engine. The same rule as `@launchfile/docker`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function namedDatabase(instance: string, name: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* `url` with its path replaced by `/<database>`, the query and fragment kept:
|
|
51
|
+
* the instance URL selects the instance database, a named use's URL selects
|
|
52
|
+
* its own.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withDatabasePath(url: string, database: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* The properties this provider registers for one declared use key of a
|
|
57
|
+
* resource it provisions, or `undefined` when it cannot cover it — a token it
|
|
58
|
+
* does not know, or a name on a use that does not repeat. `dbIndexes` carries
|
|
59
|
+
* the numbered database allocated to each `db` key when the type is redis.
|
|
60
|
+
*/
|
|
61
|
+
export declare function coverUse(type: string, key: string, base: ResourceProperties, dbIndexes: DbIndexes): Record<string, string | number> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The declared use keys this provider cannot cover for `type`, each spelled
|
|
64
|
+
* as the file spells it (`db: cache`) — the refusal's content. A name on a
|
|
65
|
+
* use that does not repeat says so, since the token itself is one this
|
|
66
|
+
* provider knows.
|
|
67
|
+
*/
|
|
68
|
+
export declare function uncoveredUses(type: string, keys: readonly string[]): string[];
|
|
69
|
+
/**
|
|
70
|
+
* The declared use keys this provider covers for `type`, in order. Used to
|
|
71
|
+
* register a resource from the pooled uses of every same-name entry: a
|
|
72
|
+
* pooled key this provider does not cover belongs to an unfulfilled
|
|
73
|
+
* `supports` entry (a `requires` one refused its component already) and
|
|
74
|
+
* registers nothing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function coveredUses(type: string, keys: readonly string[]): string[];
|
|
77
|
+
/**
|
|
78
|
+
* The entry's property map with every declared use's properties registered
|
|
79
|
+
* under `<use>.<property>` / `<use>.<name>.<property>`. Callers refuse before
|
|
80
|
+
* reaching here, so an uncovered use is a caller bug and throws.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withCoveredUses(type: string, keys: readonly string[] | undefined, base: ResourceProperties, dbIndexes: DbIndexes): ResourceProperties;
|
|
83
|
+
/**
|
|
84
|
+
* The names of the named `database` uses among `keys`, sorted and unique —
|
|
85
|
+
* the extra databases a SQL provisioner creates beside the app's. Empty for
|
|
86
|
+
* any other token, named or not.
|
|
87
|
+
*/
|
|
88
|
+
export declare function namedDatabases(keys: readonly string[]): string[];
|
|
89
|
+
/**
|
|
90
|
+
* One numbered redis database per `db` use key, app-wide, keyed by resource
|
|
91
|
+
* name — the same allocation as `@launchfile/docker` (D-65 conformance), so
|
|
92
|
+
* one file yields the same `db.*` values under each provider:
|
|
93
|
+
*
|
|
94
|
+
* - redis resources take blocks in the order their first `db`-declaring entry
|
|
95
|
+
* appears, walking components in file order and `requires` before
|
|
96
|
+
* `supports` — a `supports` entry's `db` takes its index whether or not the
|
|
97
|
+
* entry is fulfilled;
|
|
98
|
+
* - within a block the bare `db` (if any same-name entry declares it) takes
|
|
99
|
+
* the first index and the named `db` uses follow in name order, so
|
|
100
|
+
* `- db: sessions` beside `- db: cache` is the later index whichever is
|
|
101
|
+
* written first.
|
|
102
|
+
*
|
|
103
|
+
* Index 0 is the first allocated. Same-name entries pool their keys (D-24).
|
|
104
|
+
* A resource that declares no `db` use has no entry.
|
|
105
|
+
*/
|
|
106
|
+
export declare function allocateDbIndexes(launch: NormalizedLaunch): Record<string, DbIndexes>;
|
|
107
|
+
//# sourceMappingURL=uses.d.ts.map
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage of a `requires`/`supports` entry's declared `uses` (SPEC.md
|
|
3
|
+
* § Resource uses) by this provider's provisioners.
|
|
4
|
+
*
|
|
5
|
+
* A declared use is covered when the provisioner can hand over what the use
|
|
6
|
+
* asks for and register its properties under `<use>.<property>` — the dotted
|
|
7
|
+
* keys `$<resource>.<use>.<property>` resolves from. A named occurrence of a
|
|
8
|
+
* repeatable use (`- db: cache`) registers under `<use>.<name>.<property>`
|
|
9
|
+
* and is addressed as `$<resource>.<use>.<name>.<property>`. A use this
|
|
10
|
+
* provider cannot cover, a token it does not recognise, or a name on a use
|
|
11
|
+
* that does not repeat, refuses the component (PROVIDERS.md §10 item 5,
|
|
12
|
+
* D-64): no provider can claim to cover a use it does not know. This
|
|
13
|
+
* provider has no supplied-resource channel, so provision-and-cover or
|
|
14
|
+
* refuse are its only outcomes.
|
|
15
|
+
*
|
|
16
|
+
* Every function here takes **use keys** — `db` for a bare use, `db.cache`
|
|
17
|
+
* for a named one — the spelling `useKeys` from `@launchfile/sdk` produces
|
|
18
|
+
* and the resolver context lists.
|
|
19
|
+
*
|
|
20
|
+
* What each provisioner gives:
|
|
21
|
+
*
|
|
22
|
+
* - **redis** — the machine's Homebrew Redis, shared by every app on it. `db`
|
|
23
|
+
* is one numbered database on that server, allocated per use key within
|
|
24
|
+
* the app by {@link allocateDbIndexes}; `pubsub` and `server` are covered by
|
|
25
|
+
* the server itself and register nothing beyond the instance vocabulary.
|
|
26
|
+
* The server is shared across apps on this machine exactly as it is for an
|
|
27
|
+
* entry with no `uses` — a machine-wide index allocation is not attempted
|
|
28
|
+
* here.
|
|
29
|
+
* - **postgres / mysql / mariadb** — a per-app database on the local server.
|
|
30
|
+
* A bare `database` is that database; each named `database` is one more
|
|
31
|
+
* database on the server, `<instance>_<name>` ({@link namedDatabase}),
|
|
32
|
+
* created by the provisioner's own create-database path; `server` is the
|
|
33
|
+
* local instance.
|
|
34
|
+
*/
|
|
35
|
+
import { formatUseKey, parseUseKey, useKeys, } from "@launchfile/sdk";
|
|
36
|
+
/**
|
|
37
|
+
* The database a named `database` use gets on the local server:
|
|
38
|
+
* `<instance database>_<name>`, hyphens as underscores so the name is one SQL
|
|
39
|
+
* identifier on every engine. The same rule as `@launchfile/docker`.
|
|
40
|
+
*/
|
|
41
|
+
export function namedDatabase(instance, name) {
|
|
42
|
+
return `${instance}_${name.replace(/-/g, "_")}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `url` with its path replaced by `/<database>`, the query and fragment kept:
|
|
46
|
+
* the instance URL selects the instance database, a named use's URL selects
|
|
47
|
+
* its own.
|
|
48
|
+
*/
|
|
49
|
+
export function withDatabasePath(url, database) {
|
|
50
|
+
const match = /^([a-z][a-z0-9+.-]*:\/\/[^/?#]*)(?:\/[^?#]*)?(.*)$/i.exec(url);
|
|
51
|
+
if (!match)
|
|
52
|
+
return url;
|
|
53
|
+
return `${match[1]}/${database}${match[2]}`;
|
|
54
|
+
}
|
|
55
|
+
const SQL_SERVER_USES = {
|
|
56
|
+
database: {
|
|
57
|
+
repeatable: true,
|
|
58
|
+
cover: ({ base, name }) => {
|
|
59
|
+
if (name === undefined) {
|
|
60
|
+
return { "database.url": base.url, "database.name": base.name ?? "" };
|
|
61
|
+
}
|
|
62
|
+
const database = namedDatabase(base.name ?? "", name);
|
|
63
|
+
return {
|
|
64
|
+
[`database.${name}.url`]: withDatabasePath(base.url, database),
|
|
65
|
+
[`database.${name}.name`]: database,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
server: { repeatable: false, cover: () => ({}) },
|
|
70
|
+
};
|
|
71
|
+
const COVERAGE = {
|
|
72
|
+
redis: {
|
|
73
|
+
db: {
|
|
74
|
+
repeatable: true,
|
|
75
|
+
cover: ({ base, name, dbIndex }) => {
|
|
76
|
+
// The instance url may already select database 0; the use's
|
|
77
|
+
// url selects the allocated index instead.
|
|
78
|
+
const instance = base.url.replace(/\/\d+$/, "");
|
|
79
|
+
const prefix = name === undefined ? "db" : `db.${name}`;
|
|
80
|
+
return { [`${prefix}.url`]: `${instance}/${dbIndex}`, [`${prefix}.index`]: dbIndex };
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
pubsub: { repeatable: false, cover: () => ({}) },
|
|
84
|
+
server: { repeatable: false, cover: () => ({}) },
|
|
85
|
+
},
|
|
86
|
+
postgres: SQL_SERVER_USES,
|
|
87
|
+
mysql: SQL_SERVER_USES,
|
|
88
|
+
mariadb: SQL_SERVER_USES,
|
|
89
|
+
};
|
|
90
|
+
function coverageOf(type, key) {
|
|
91
|
+
const uses = Object.hasOwn(COVERAGE, type) ? COVERAGE[type] : undefined;
|
|
92
|
+
const { use, name } = parseUseKey(key);
|
|
93
|
+
const coverage = uses && Object.hasOwn(uses, use) ? uses[use] : undefined;
|
|
94
|
+
if (!coverage)
|
|
95
|
+
return undefined;
|
|
96
|
+
return name !== undefined && !coverage.repeatable ? undefined : coverage;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The properties this provider registers for one declared use key of a
|
|
100
|
+
* resource it provisions, or `undefined` when it cannot cover it — a token it
|
|
101
|
+
* does not know, or a name on a use that does not repeat. `dbIndexes` carries
|
|
102
|
+
* the numbered database allocated to each `db` key when the type is redis.
|
|
103
|
+
*/
|
|
104
|
+
export function coverUse(type, key, base, dbIndexes) {
|
|
105
|
+
const coverage = coverageOf(type, key);
|
|
106
|
+
if (!coverage)
|
|
107
|
+
return undefined;
|
|
108
|
+
const { name } = parseUseKey(key);
|
|
109
|
+
const dbIndex = Object.hasOwn(dbIndexes, key) ? dbIndexes[key] : 0;
|
|
110
|
+
return coverage.cover({ base, name, dbIndex });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The declared use keys this provider cannot cover for `type`, each spelled
|
|
114
|
+
* as the file spells it (`db: cache`) — the refusal's content. A name on a
|
|
115
|
+
* use that does not repeat says so, since the token itself is one this
|
|
116
|
+
* provider knows.
|
|
117
|
+
*/
|
|
118
|
+
export function uncoveredUses(type, keys) {
|
|
119
|
+
const uncovered = [];
|
|
120
|
+
for (const key of keys) {
|
|
121
|
+
if (coverageOf(type, key) !== undefined)
|
|
122
|
+
continue;
|
|
123
|
+
const { use, name } = parseUseKey(key);
|
|
124
|
+
const known = Object.hasOwn(COVERAGE, type) && Object.hasOwn(COVERAGE[type], use);
|
|
125
|
+
uncovered.push(name !== undefined && known
|
|
126
|
+
? `${formatUseKey(key)} (${use} on ${type} is not repeatable and takes no name)`
|
|
127
|
+
: formatUseKey(key));
|
|
128
|
+
}
|
|
129
|
+
return uncovered;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The declared use keys this provider covers for `type`, in order. Used to
|
|
133
|
+
* register a resource from the pooled uses of every same-name entry: a
|
|
134
|
+
* pooled key this provider does not cover belongs to an unfulfilled
|
|
135
|
+
* `supports` entry (a `requires` one refused its component already) and
|
|
136
|
+
* registers nothing.
|
|
137
|
+
*/
|
|
138
|
+
export function coveredUses(type, keys) {
|
|
139
|
+
return keys.filter((key) => coverageOf(type, key) !== undefined);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The entry's property map with every declared use's properties registered
|
|
143
|
+
* under `<use>.<property>` / `<use>.<name>.<property>`. Callers refuse before
|
|
144
|
+
* reaching here, so an uncovered use is a caller bug and throws.
|
|
145
|
+
*/
|
|
146
|
+
export function withCoveredUses(type, keys, base, dbIndexes) {
|
|
147
|
+
const properties = { ...base };
|
|
148
|
+
for (const key of keys ?? []) {
|
|
149
|
+
const covered = coverUse(type, key, base, dbIndexes);
|
|
150
|
+
if (covered === undefined) {
|
|
151
|
+
throw new Error(`withCoveredUses: use "${formatUseKey(key)}" on ${type} is not covered — the caller must refuse first`);
|
|
152
|
+
}
|
|
153
|
+
Object.assign(properties, covered);
|
|
154
|
+
}
|
|
155
|
+
return properties;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The names of the named `database` uses among `keys`, sorted and unique —
|
|
159
|
+
* the extra databases a SQL provisioner creates beside the app's. Empty for
|
|
160
|
+
* any other token, named or not.
|
|
161
|
+
*/
|
|
162
|
+
export function namedDatabases(keys) {
|
|
163
|
+
const names = new Set();
|
|
164
|
+
for (const key of keys) {
|
|
165
|
+
const { use, name } = parseUseKey(key);
|
|
166
|
+
if (use === "database" && name !== undefined)
|
|
167
|
+
names.add(name);
|
|
168
|
+
}
|
|
169
|
+
return [...names].sort();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* One numbered redis database per `db` use key, app-wide, keyed by resource
|
|
173
|
+
* name — the same allocation as `@launchfile/docker` (D-65 conformance), so
|
|
174
|
+
* one file yields the same `db.*` values under each provider:
|
|
175
|
+
*
|
|
176
|
+
* - redis resources take blocks in the order their first `db`-declaring entry
|
|
177
|
+
* appears, walking components in file order and `requires` before
|
|
178
|
+
* `supports` — a `supports` entry's `db` takes its index whether or not the
|
|
179
|
+
* entry is fulfilled;
|
|
180
|
+
* - within a block the bare `db` (if any same-name entry declares it) takes
|
|
181
|
+
* the first index and the named `db` uses follow in name order, so
|
|
182
|
+
* `- db: sessions` beside `- db: cache` is the later index whichever is
|
|
183
|
+
* written first.
|
|
184
|
+
*
|
|
185
|
+
* Index 0 is the first allocated. Same-name entries pool their keys (D-24).
|
|
186
|
+
* A resource that declares no `db` use has no entry.
|
|
187
|
+
*/
|
|
188
|
+
export function allocateDbIndexes(launch) {
|
|
189
|
+
const pooled = new Map();
|
|
190
|
+
for (const component of Object.values(launch.components)) {
|
|
191
|
+
for (const entry of [...(component.requires ?? []), ...(component.supports ?? [])]) {
|
|
192
|
+
if (entry.host || entry.type !== "redis" || !entry.uses)
|
|
193
|
+
continue;
|
|
194
|
+
const dbKeys = useKeys(entry.uses).filter((key) => parseUseKey(key).use === "db");
|
|
195
|
+
if (dbKeys.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
const key = entry.name ?? entry.type;
|
|
198
|
+
const keys = pooled.get(key) ?? new Set();
|
|
199
|
+
for (const dbKey of dbKeys)
|
|
200
|
+
keys.add(dbKey);
|
|
201
|
+
pooled.set(key, keys);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const allocation = {};
|
|
205
|
+
let next = 0;
|
|
206
|
+
for (const [resource, keys] of pooled) {
|
|
207
|
+
const indexes = {};
|
|
208
|
+
if (keys.has("db"))
|
|
209
|
+
indexes.db = next++;
|
|
210
|
+
for (const key of [...keys].filter((k) => k !== "db").sort()) {
|
|
211
|
+
indexes[key] = next++;
|
|
212
|
+
}
|
|
213
|
+
allocation[resource] = indexes;
|
|
214
|
+
}
|
|
215
|
+
return allocation;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=uses.js.map
|
package/dist/state.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Persists secrets, ports, and resource state in .launchfile/state.json
|
|
5
5
|
* so credentials and ports are stable across restarts.
|
|
6
6
|
*/
|
|
7
|
+
import type { DbIndexes } from "./resources/uses.js";
|
|
7
8
|
export interface ResourceState {
|
|
8
9
|
type: string;
|
|
9
10
|
name: string;
|
|
@@ -12,7 +13,31 @@ export interface ResourceState {
|
|
|
12
13
|
dbName?: string;
|
|
13
14
|
user?: string;
|
|
14
15
|
password?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The numbered database `up` allocated to this resource for its bare redis
|
|
18
|
+
* `db` use (SPEC.md § Resource uses). Recorded so `env` registers the same
|
|
19
|
+
* `db.url` / `db.index` the running app was given, rather than re-deriving
|
|
20
|
+
* an index from a file that may have changed since. Absent on a resource
|
|
21
|
+
* with no bare `db` use and on state written before the index was recorded.
|
|
22
|
+
*/
|
|
23
|
+
dbIndex?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The numbered database allocated to each named redis `db` use, by name
|
|
26
|
+
* (`- db: cache` → `{ cache: 1 }`), recorded for the same reason as
|
|
27
|
+
* `dbIndex`. Absent on a resource with no named `db` use.
|
|
28
|
+
*/
|
|
29
|
+
namedDbIndexes?: Record<string, number>;
|
|
30
|
+
/**
|
|
31
|
+
* The extra databases the provisioner created for named `database` uses
|
|
32
|
+
* (`<instance>_<name>`), so `destroy` drops what `up` created. Absent on a
|
|
33
|
+
* resource with no named `database` use.
|
|
34
|
+
*/
|
|
35
|
+
databases?: string[];
|
|
15
36
|
}
|
|
37
|
+
/** The `db` use keys → index map `up` recorded on a resource: `db` from `dbIndex`, `db.<name>` from `namedDbIndexes`. */
|
|
38
|
+
export declare function recordedDbIndexes(res: ResourceState): DbIndexes;
|
|
39
|
+
/** `res` with the allocated `db` indexes recorded — the inverse of {@link recordedDbIndexes}. Records nothing for an empty allocation. */
|
|
40
|
+
export declare function withRecordedDbIndexes(res: ResourceState, indexes: DbIndexes): ResourceState;
|
|
16
41
|
/**
|
|
17
42
|
* Records a spawned app component process so `launch down` can stop it
|
|
18
43
|
* from a different shell or after the foreground `launch up` session ends.
|
|
@@ -72,6 +97,17 @@ export interface LaunchState {
|
|
|
72
97
|
* written before this existed omit it.
|
|
73
98
|
*/
|
|
74
99
|
operatorStorage?: Record<string, Record<string, string>>;
|
|
100
|
+
/**
|
|
101
|
+
* Orchestrator-supplied publication context (D-58): the normalized public
|
|
102
|
+
* URL `$app.*` resolves from, persisted alongside `ports` so `env` and
|
|
103
|
+
* `bootstrap` resolve the same values as the `up` that set it. A later `up`
|
|
104
|
+
* that supplies a different value replaces it and the derived env recomputes
|
|
105
|
+
* (D-49); one that omits it preserves what is recorded — the same rule
|
|
106
|
+
* `@launchfile/docker` applies to its own `appUrl`. Optional for backward
|
|
107
|
+
* compatibility — absent means this provider's own localhost routing
|
|
108
|
+
* answers.
|
|
109
|
+
*/
|
|
110
|
+
appUrl?: string;
|
|
75
111
|
}
|
|
76
112
|
export declare function hashLaunchfile(content: string): string;
|
|
77
113
|
/** Load state from disk, or return null if none exists */
|
package/dist/state.js
CHANGED
|
@@ -4,10 +4,36 @@
|
|
|
4
4
|
* Persists secrets, ports, and resource state in .launchfile/state.json
|
|
5
5
|
* so credentials and ports are stable across restarts.
|
|
6
6
|
*/
|
|
7
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
7
|
+
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { registerSecrets } from "./redact.js";
|
|
11
|
+
/** The `db` use keys → index map `up` recorded on a resource: `db` from `dbIndex`, `db.<name>` from `namedDbIndexes`. */
|
|
12
|
+
export function recordedDbIndexes(res) {
|
|
13
|
+
const indexes = {};
|
|
14
|
+
if (res.dbIndex !== undefined)
|
|
15
|
+
indexes.db = res.dbIndex;
|
|
16
|
+
for (const [name, index] of Object.entries(res.namedDbIndexes ?? {})) {
|
|
17
|
+
indexes[`db.${name}`] = index;
|
|
18
|
+
}
|
|
19
|
+
return indexes;
|
|
20
|
+
}
|
|
21
|
+
/** `res` with the allocated `db` indexes recorded — the inverse of {@link recordedDbIndexes}. Records nothing for an empty allocation. */
|
|
22
|
+
export function withRecordedDbIndexes(res, indexes) {
|
|
23
|
+
const named = {};
|
|
24
|
+
let dbIndex;
|
|
25
|
+
for (const [key, index] of Object.entries(indexes)) {
|
|
26
|
+
if (key === "db")
|
|
27
|
+
dbIndex = index;
|
|
28
|
+
else
|
|
29
|
+
named[key.slice("db.".length)] = index;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...res,
|
|
33
|
+
...(dbIndex !== undefined ? { dbIndex } : {}),
|
|
34
|
+
...(Object.keys(named).length > 0 ? { namedDbIndexes: named } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
11
37
|
const STATE_DIR = ".launchfile";
|
|
12
38
|
const STATE_FILE = "state.json";
|
|
13
39
|
function stateDir(projectDir) {
|
|
@@ -64,8 +90,15 @@ export async function saveState(projectDir, state) {
|
|
|
64
90
|
/** Ensure .launchfile directories exist */
|
|
65
91
|
export async function ensureDirs(projectDir) {
|
|
66
92
|
const dirs = ["storage", "tmp", "logs", "data", "env"];
|
|
67
|
-
// Security:
|
|
68
|
-
|
|
93
|
+
// Security: these dirs hold secrets, logs, and env files. mkdir applies the
|
|
94
|
+
// mode only when it creates the directory, so chmod unconditionally — a dir
|
|
95
|
+
// left by an earlier version or a looser umask must not stay world-readable
|
|
96
|
+
// (CWE-276). Mirrors packages/launchfile/src/state/errors.ts.
|
|
97
|
+
await Promise.all(dirs.map(async (d) => {
|
|
98
|
+
const dir = join(projectDir, STATE_DIR, d);
|
|
99
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
100
|
+
await chmod(dir, 0o700);
|
|
101
|
+
}));
|
|
69
102
|
// Safety net: write a .gitignore inside .launchfile/ so secrets aren't
|
|
70
103
|
// accidentally committed even if the project's .gitignore doesn't exclude it.
|
|
71
104
|
await writeFile(join(projectDir, STATE_DIR, ".gitignore"), "*\n", { mode: 0o644 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchfile/macos-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "macOS dev provider for Launchfile — run apps locally via brew services and native runtimes",
|
|
5
5
|
"os": [
|
|
6
6
|
"darwin"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"directory": "providers/macos-dev"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@launchfile/sdk": "^0.
|
|
39
|
+
"@launchfile/sdk": "^0.10.0",
|
|
40
40
|
"semver": "^7.7.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|