@starci/hfs 5.1.0 → 5.2.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/CHANGELOG.md +20 -0
- package/README.md +10 -1
- package/db/postgres.mjs +9 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +30 -9
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +55 -7
- package/runtime/knowledge/hfs/slots.yaml +75 -24
- package/runtime/modules/kernel/failure-codes.yaml +62 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
- package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
- package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
- package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
- package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
- package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
- package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
- package/runtime/scripts/hfs/secret.mjs +10 -7
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
- package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +49 -0
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.mjs +7 -7
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -0
- package/src/main.mjs +23 -2
- package/stack/cli.mjs +6 -1
- package/stack/context.d.mts +15 -0
- package/stack/context.mjs +1 -0
- package/stack/local-cli.d.mts +25 -0
- package/stack/local-cli.mjs +89 -0
- package/stack/local-config.d.mts +17 -0
- package/stack/local-config.mjs +37 -0
- package/stack/local-custody.d.mts +24 -0
- package/stack/local-custody.mjs +109 -0
- package/stack/local-doctor.d.mts +6 -0
- package/stack/local-doctor.mjs +49 -0
- package/stack/local-down.d.mts +8 -0
- package/stack/local-down.mjs +20 -0
- package/stack/local-host.d.mts +42 -0
- package/stack/local-host.mjs +163 -0
- package/stack/local-logs.d.mts +8 -0
- package/stack/local-logs.mjs +32 -0
- package/stack/local-ports.d.mts +7 -0
- package/stack/local-ports.mjs +39 -0
- package/stack/local-prepare.d.mts +22 -0
- package/stack/local-prepare.mjs +62 -0
- package/stack/local-run.d.mts +71 -0
- package/stack/local-run.mjs +200 -0
- package/stack/local-state.d.mts +35 -0
- package/stack/local-state.mjs +29 -0
- package/stack/local-status.d.mts +6 -0
- package/stack/local-status.mjs +36 -0
- package/stack/local-up.d.mts +11 -0
- package/stack/local-up.mjs +166 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -0
- package/sync/index.d.mts +13 -12
- package/sync/index.mjs +83 -28
- package/templates/app/module/jest.config.js.tpl +8 -0
- package/templates/app/module/src/index.ts.tpl +2 -0
- package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
- package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
- package/templates/app/module/src/manifest/index.ts.tpl +1 -0
- package/templates/app/module/src/public/index.ts.tpl +2 -0
- package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
- package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
- package/templates/app/module/src/template/index.ts.tpl +1 -0
- package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
- package/templates/app/module/src/workbench/index.ts.tpl +2 -0
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +90 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +17 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +78 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +10 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -4
- package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- package/templates/fe/common/site-shell.tsx.partial +3 -3
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
|
@@ -24,6 +24,7 @@ import { setValues } from "./secret-prompt.mjs";
|
|
|
24
24
|
import { stackContext } from "../../../stack/context.mjs";
|
|
25
25
|
import { StackError } from "../../../stack/errors.mjs";
|
|
26
26
|
import { serverKey } from "../../../stack/server-key.mjs";
|
|
27
|
+
import { seedDevCustody } from "./secret-dev-init.mjs";
|
|
27
28
|
export { envelopeOf, formatOf, plaintextOf } from "./secret-document.mjs";
|
|
28
29
|
export { setSecretValues } from "./secret-store.mjs";
|
|
29
30
|
/** The actions of `starci app secret`, in usage order. */
|
|
@@ -76,7 +77,7 @@ function parse(argv) {
|
|
|
76
77
|
/** The operands and flags of one secret action, as the usage of `starci app secret` prints them. */
|
|
77
78
|
export function secretUsage(verb) {
|
|
78
79
|
if (verb === "init")
|
|
79
|
-
return "";
|
|
80
|
+
return "[--env <dev env> [--execute]]";
|
|
80
81
|
if (verb === "set")
|
|
81
82
|
return "--env <env> <slug> <KEY> [KEY ...] [--generate <bytes>] [--from-file KEY=path ...]";
|
|
82
83
|
if (verb === "get")
|
|
@@ -100,8 +101,8 @@ export function secretUsage(verb) {
|
|
|
100
101
|
return "--env <env>";
|
|
101
102
|
}
|
|
102
103
|
function editOptionsFor(verb, opts) {
|
|
103
|
-
if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients")
|
|
104
|
-
throw new SecretError("--execute is only valid for unset, rename, remove, recipients, migrate, split and server-key");
|
|
104
|
+
if (opts.execute !== undefined && !EDIT_VERBS.has(verb) && !MOVE_VERBS.has(verb) && verb !== "server-key" && verb !== "recipients" && verb !== "init")
|
|
105
|
+
throw new SecretError("--execute is only valid for init --env, unset, rename, remove, recipients, migrate, split and server-key");
|
|
105
106
|
if (opts.execute !== undefined && verb === "recipients" && !opts.add && !opts.remove)
|
|
106
107
|
throw new SecretError("--execute is only valid for recipients with --add or --remove");
|
|
107
108
|
if ((opts.rotate !== undefined || opts.backup !== undefined || opts.restore !== undefined) && verb !== "server-key")
|
|
@@ -128,8 +129,8 @@ function outputOptionsFor(verb, opts) {
|
|
|
128
129
|
}
|
|
129
130
|
/** The environment leads the operands (`--env <env> <slug> ...` is checked and read as `<env> <slug> ...`); init takes none, audit may name one. */
|
|
130
131
|
function environmentOf(verb, opts) {
|
|
131
|
-
if (verb === "init" && opts.env !== undefined)
|
|
132
|
-
throw new SecretError("--
|
|
132
|
+
if (verb === "init" && opts.env === undefined && opts.execute !== undefined)
|
|
133
|
+
throw new SecretError("--execute is only valid for init --env <dev env>");
|
|
133
134
|
if (verb !== "init" && verb !== "audit" && opts.env === undefined)
|
|
134
135
|
throw new SecretError(`usage: starci app secret ${verb} ${secretUsage(verb)}`);
|
|
135
136
|
if (verb === "audit" && opts.positional.length)
|
|
@@ -141,7 +142,7 @@ function optionsFor(verb, opts) {
|
|
|
141
142
|
environmentOf(verb, opts);
|
|
142
143
|
const count = opts.positional.length;
|
|
143
144
|
const bounds = {
|
|
144
|
-
init: [0,
|
|
145
|
+
init: [0, 1],
|
|
145
146
|
list: [1, 1],
|
|
146
147
|
materialize: [1, 1],
|
|
147
148
|
verify: [1, 1],
|
|
@@ -253,10 +254,12 @@ export async function secretMain(argv, { stdout = (s) => process.stdout.write(s)
|
|
|
253
254
|
const [verb, ...rest] = argv;
|
|
254
255
|
try {
|
|
255
256
|
if (!VERBS.has(verb))
|
|
256
|
-
throw new SecretError("usage: starci app secret init | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
|
|
257
|
+
throw new SecretError("usage: starci app secret init [--env <dev env> [--execute]] | set --env <env> <slug> <KEY> [KEY ...] | get --env <env> <slug> [KEY] --to-file | unset --env <env> <slug> <KEY> | rename --env <env> <slug> <OLD> <NEW> | remove --env <env> <slug> | materialize --env <env> | list --env <env> | verify --env <env> | recipients --env <env> [--add|--remove ...] [--execute] | migrate --env <env> <slug> [--to <slug>] [--into-existing] | split --env <env> <slug> [--group KEY=member] [--group-file <yaml>] [--into-existing] | audit [--env <env>] | server-key --env <env> [--rotate | --backup github | --restore github] [--execute]");
|
|
257
258
|
const opts = parse(rest);
|
|
258
259
|
optionsFor(verb, opts);
|
|
259
260
|
const repoRoot = path.resolve(cwd, opts.cwd ?? ".");
|
|
261
|
+
if (verb === "init" && opts.env !== undefined)
|
|
262
|
+
return seedDevCustody(repoRoot, opts.env, { execute: opts.execute === true, sops: () => sops ?? defaultSops(env, repoRoot, platform, tools, hostIdentity), stdout, random });
|
|
260
263
|
if (verb === "init")
|
|
261
264
|
return publicStorage(() => initializeSecrets(repoRoot, { env, platform, tools, stdout, host: hostIdentity }));
|
|
262
265
|
if (verb === "audit")
|
|
@@ -232,7 +232,7 @@ export function readRepoDeclaration(manifest, repoRoot) {
|
|
|
232
232
|
function createScopeResolver(manifest, repo) {
|
|
233
233
|
const profile = repo.profile;
|
|
234
234
|
const edition = repo.edition ?? 'full';
|
|
235
|
-
const slots = manifest.slots.filter((s) => s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
235
|
+
const slots = manifest.slots.filter((s) => s.managedOnly !== true && s.profiles.includes(profile) && slotInEdition(manifest, s, edition)).map((s) => effectiveSlot(s, profile, edition));
|
|
236
236
|
const scope = { manifest, repo, profile, slots, byId: new Map(slots.map((s) => [s.id, s])), variants: slots.flatMap(variantsOf), appKind: new Map(repo.apps.map((a) => [a.name, a.kind])) };
|
|
237
237
|
const classifyPath = (input) => classifyIn(scope, input);
|
|
238
238
|
/** tracked | ignored | external for the slot owning `p`, or null when no slot owns it. */
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { StackProblem } from './stack-config-kit.mjs';
|
|
2
|
+
export declare const LOCAL_CONFIG_CODE = "STACKS_LOCAL_CONFIG";
|
|
3
|
+
/** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
|
|
4
|
+
export declare const GENERATORS: readonly string[];
|
|
5
|
+
/** A key of a sealed member of the environment. */
|
|
6
|
+
export interface CustodyRef {
|
|
7
|
+
member: string;
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
/** A command the verbs run: an argv list (never a shell line), run from `cwd` (app-root relative). */
|
|
11
|
+
export interface LocalCommand {
|
|
12
|
+
name: string;
|
|
13
|
+
argv: string[];
|
|
14
|
+
cwd: string;
|
|
15
|
+
heavy: 'install' | 'build' | 'none';
|
|
16
|
+
skipIfExists: string | null;
|
|
17
|
+
env: Record<string, string>;
|
|
18
|
+
files: string[];
|
|
19
|
+
/** Seed only: `migrate` runs it once the database is migrated, `ready` once the processes answer (a seed that signs up through the api). */
|
|
20
|
+
after: 'migrate' | 'ready';
|
|
21
|
+
}
|
|
22
|
+
/** A compose service the environment starts and how its readiness is observed. */
|
|
23
|
+
export interface LocalService {
|
|
24
|
+
name: string;
|
|
25
|
+
wait: 'healthy' | 'started' | 'tcp';
|
|
26
|
+
port: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A native app process. */
|
|
29
|
+
export interface LocalProcess extends LocalCommand {
|
|
30
|
+
port: string | null;
|
|
31
|
+
health: HttpProbe | null;
|
|
32
|
+
}
|
|
33
|
+
/** One HTTP probe: the readiness of a process, a `ready` row or a `verify` row. */
|
|
34
|
+
export interface HttpProbe {
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
method: string;
|
|
38
|
+
status: number;
|
|
39
|
+
contains: string | null;
|
|
40
|
+
}
|
|
41
|
+
/** The per-environment database: its compose service, its own login role and where the app reads its URL. */
|
|
42
|
+
export interface LocalDatabase {
|
|
43
|
+
service: string;
|
|
44
|
+
port: string;
|
|
45
|
+
name: string;
|
|
46
|
+
role: string;
|
|
47
|
+
urlEnv: string;
|
|
48
|
+
urlAs: 'env' | 'file';
|
|
49
|
+
adminUser: string;
|
|
50
|
+
password: CustodyRef;
|
|
51
|
+
adminPassword: CustodyRef;
|
|
52
|
+
appRole: string | null;
|
|
53
|
+
appPassword: CustodyRef | null;
|
|
54
|
+
}
|
|
55
|
+
/** One dev-only value the owner-armed `secret init` creates once. */
|
|
56
|
+
export interface CustodyGenerator extends CustodyRef {
|
|
57
|
+
kind: string;
|
|
58
|
+
bytes: number;
|
|
59
|
+
publicKey: string | null;
|
|
60
|
+
}
|
|
61
|
+
/** The normalised `local` block of one environment. */
|
|
62
|
+
export interface LocalBlock {
|
|
63
|
+
project: string;
|
|
64
|
+
ports: Record<string, number>;
|
|
65
|
+
services: LocalService[];
|
|
66
|
+
database: LocalDatabase | null;
|
|
67
|
+
composeEnv: Array<CustodyRef & {
|
|
68
|
+
name: string;
|
|
69
|
+
}>;
|
|
70
|
+
/** Least grants: the keys one compose service may read, mounted from `STACK_GRANT_<NAME>` (a directory holding only those keys). */
|
|
71
|
+
grants: Array<{
|
|
72
|
+
name: string;
|
|
73
|
+
keys: string[];
|
|
74
|
+
}>;
|
|
75
|
+
/** Data directories under the state base (`${data}/<name>`, `STACK_DATA_DIR`), created by `up` before anything starts: bind mounts and file stores of native processes. */
|
|
76
|
+
dirs: string[];
|
|
77
|
+
custody: CustodyGenerator[];
|
|
78
|
+
setup: LocalCommand[];
|
|
79
|
+
seed: LocalCommand[];
|
|
80
|
+
processes: LocalProcess[];
|
|
81
|
+
ready: HttpProbe[];
|
|
82
|
+
verify: HttpProbe[];
|
|
83
|
+
origins: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
url: string;
|
|
86
|
+
}>;
|
|
87
|
+
resources: {
|
|
88
|
+
diskGb: number;
|
|
89
|
+
ramMb: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
|
|
93
|
+
export declare function normalizeLocal(raw: unknown, { project, env }: {
|
|
94
|
+
project: string;
|
|
95
|
+
env: string;
|
|
96
|
+
}): {
|
|
97
|
+
config: LocalBlock | null;
|
|
98
|
+
problems: StackProblem[];
|
|
99
|
+
};
|
|
100
|
+
/** Replaces `${port.<name>}` with the chosen host port. */
|
|
101
|
+
export declare const expandPorts: (value: string, ports: Readonly<Record<string, number>>) => string;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// stack-local-config.mjs - reads and judges `environments.<env>.local`, the declaration of a dev environment (runtime docker-compose) that
|
|
2
|
+
// `starci app stack up|down|doctor|prepare|status|logs|verify --env <env>` runs (PROJECT-STACKS-6). It holds names, ports, argv lists and
|
|
3
|
+
// shapes only, never a value: custody keys are references into .starcistacks/<env>/secrets (PROJECT-STACKS-5). The authority for the fields is
|
|
4
|
+
// this module; the same normaliser serves `starci app check` (rule R340) and the verbs, so a block the check passes is a block the verbs run.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { isPlainObject as plain } from '../../engine/plain-object.mjs';
|
|
7
|
+
import { closedFields, collector, patternField } from './stack-config-kit.mjs';
|
|
8
|
+
export const LOCAL_CONFIG_CODE = 'STACKS_LOCAL_CONFIG';
|
|
9
|
+
const NAME = /^[a-z][a-z0-9-]{0,40}$/u;
|
|
10
|
+
const KEY = /^[A-Z][A-Z0-9_]{0,63}$/u;
|
|
11
|
+
const PROJECT = /^[a-z0-9][a-z0-9_-]{0,62}$/u;
|
|
12
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]{0,40}$/u;
|
|
13
|
+
const SQL_NAME = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
14
|
+
/** The generators `starci app secret init --env <env>` knows (PROJECT-STACKS-5: dev-only values, created once, never replaced). */
|
|
15
|
+
export const GENERATORS = Object.freeze(['hex', 'base64', 'password', 'uuid', 'rsa-pem', 'ec-pem', 'ed25519-pem']);
|
|
16
|
+
const TOP = ['project', 'ports', 'services', 'database', 'composeEnv', 'grants', 'dirs', 'custody', 'setup', 'seed', 'processes', 'ready', 'verify', 'origins', 'resources'];
|
|
17
|
+
const COMMAND = ['name', 'argv', 'cwd', 'heavy', 'skipIfExists', 'env', 'files'];
|
|
18
|
+
const SEED = [...COMMAND, 'after'];
|
|
19
|
+
const PROCESS = [...COMMAND, 'port', 'health'];
|
|
20
|
+
const text = (value) => typeof value === 'string' && value.trim() !== '';
|
|
21
|
+
/** A relative path inside the app root: no drive, no `..`, forward slashes. */
|
|
22
|
+
function relative(c, at, value) {
|
|
23
|
+
if (typeof value !== 'string' || value === '' || path.posix.isAbsolute(value) || /^[A-Za-z]:/u.test(value) || value.includes('\\') || path.posix.normalize(value).split('/').includes('..')) {
|
|
24
|
+
c.problem(at, 'must be a relative path inside the app root');
|
|
25
|
+
return '.';
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function ref(c, at, value) {
|
|
30
|
+
if (!closedFields(c, at, value, ['member', 'key'], 'local'))
|
|
31
|
+
return { member: '', key: '' };
|
|
32
|
+
const entry = value;
|
|
33
|
+
patternField(c, `${at}.member`, entry.member, /^[a-z0-9][a-z0-9-]*$/u);
|
|
34
|
+
patternField(c, `${at}.key`, entry.key, KEY);
|
|
35
|
+
return { member: String(entry.member), key: String(entry.key) };
|
|
36
|
+
}
|
|
37
|
+
function strings(c, at, value, pattern = null) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
if (!Array.isArray(value) || value.some((item) => !text(item))) {
|
|
41
|
+
c.problem(at, 'must be a list of non-empty strings');
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
if (pattern)
|
|
45
|
+
value.forEach((item, index) => patternField(c, `${at}[${index}]`, item, pattern));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function command(c, at, value, allowed, names) {
|
|
49
|
+
const empty = { name: '', argv: [], cwd: '.', heavy: 'none', skipIfExists: null, env: {}, files: [], after: 'migrate' };
|
|
50
|
+
if (!closedFields(c, at, value, allowed, 'local'))
|
|
51
|
+
return empty;
|
|
52
|
+
const entry = value;
|
|
53
|
+
const name = String(entry.name ?? '');
|
|
54
|
+
if (patternField(c, `${at}.name`, entry.name, NAME)) {
|
|
55
|
+
if (names.has(name))
|
|
56
|
+
c.problem(`${at}.name`, `${name} is declared twice`);
|
|
57
|
+
names.add(name);
|
|
58
|
+
}
|
|
59
|
+
const argv = strings(c, `${at}.argv`, entry.argv);
|
|
60
|
+
if (argv.length === 0)
|
|
61
|
+
c.problem(`${at}.argv`, 'must hold the command and its arguments (an argv list, never a shell line)');
|
|
62
|
+
const heavy = entry.heavy ?? 'none';
|
|
63
|
+
if (!['install', 'build', 'none'].includes(String(heavy)))
|
|
64
|
+
c.problem(`${at}.heavy`, 'must be install, build or none (the heavy-command governor class of the command)');
|
|
65
|
+
let env = {};
|
|
66
|
+
if (entry.env !== undefined) {
|
|
67
|
+
if (!plain(entry.env))
|
|
68
|
+
c.problem(`${at}.env`, 'must be a mapping of non-secret values');
|
|
69
|
+
else {
|
|
70
|
+
env = entry.env;
|
|
71
|
+
for (const [key, item] of Object.entries(env)) {
|
|
72
|
+
patternField(c, `${at}.env.${key}`, key, KEY);
|
|
73
|
+
if (typeof item !== 'string')
|
|
74
|
+
c.problem(`${at}.env.${key}`, 'must be a string');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name, argv, cwd: entry.cwd === undefined ? '.' : relative(c, `${at}.cwd`, entry.cwd), heavy: heavy,
|
|
80
|
+
skipIfExists: entry.skipIfExists === undefined ? null : relative(c, `${at}.skipIfExists`, entry.skipIfExists), env,
|
|
81
|
+
files: strings(c, `${at}.files`, entry.files, KEY), after: afterOf(c, at, entry.after),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function afterOf(c, at, value) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
return 'migrate';
|
|
87
|
+
if (value !== 'migrate' && value !== 'ready')
|
|
88
|
+
c.problem(`${at}.after`, 'must be migrate or ready');
|
|
89
|
+
return value === 'ready' ? 'ready' : 'migrate';
|
|
90
|
+
}
|
|
91
|
+
function probe(c, at, value) {
|
|
92
|
+
const empty = { name: '', url: '', method: 'GET', status: 200, contains: null };
|
|
93
|
+
if (!closedFields(c, at, value, ['name', 'url', 'method', 'status', 'contains'], 'local'))
|
|
94
|
+
return empty;
|
|
95
|
+
const entry = value;
|
|
96
|
+
patternField(c, `${at}.name`, entry.name, NAME);
|
|
97
|
+
const url = String(entry.url ?? '');
|
|
98
|
+
if (!/^http:\/\/(localhost|127\.0\.0\.1|[a-z0-9-]+\.localhost)(:\$\{port\.[a-z][a-z0-9-]*\}|:[0-9]+)?(\/.*)?$/u.test(url))
|
|
99
|
+
c.problem(`${at}.url`, 'must be an http URL on the loopback (localhost, 127.0.0.1 or *.localhost), its port a literal or ${port.<name>}');
|
|
100
|
+
const method = String(entry.method ?? 'GET');
|
|
101
|
+
if (!['GET', 'HEAD', 'POST'].includes(method))
|
|
102
|
+
c.problem(`${at}.method`, 'must be GET, HEAD or POST');
|
|
103
|
+
const status = entry.status === undefined ? 200 : entry.status;
|
|
104
|
+
if (!Number.isInteger(status) || status < 100 || status > 599)
|
|
105
|
+
c.problem(`${at}.status`, 'must be an HTTP status');
|
|
106
|
+
return { name: String(entry.name ?? ''), url, method, status: status, contains: text(entry.contains) ? entry.contains : null };
|
|
107
|
+
}
|
|
108
|
+
function portsOf(c, value) {
|
|
109
|
+
const ports = {};
|
|
110
|
+
if (!plain(value) || Object.keys(value).length === 0) {
|
|
111
|
+
c.problem('ports', 'must name at least one port: the host ports the environment publishes or listens on');
|
|
112
|
+
return ports;
|
|
113
|
+
}
|
|
114
|
+
const seen = new Map();
|
|
115
|
+
for (const [name, port] of Object.entries(value)) {
|
|
116
|
+
patternField(c, `ports.${name}`, name, NAME);
|
|
117
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
|
|
118
|
+
c.problem(`ports.${name}`, 'must be a port from 1024 to 65535');
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (seen.has(port))
|
|
122
|
+
c.problem(`ports.${name}`, `is the port of ${seen.get(port)} too`);
|
|
123
|
+
seen.set(port, name);
|
|
124
|
+
ports[name] = port;
|
|
125
|
+
}
|
|
126
|
+
return ports;
|
|
127
|
+
}
|
|
128
|
+
function servicesOf(c, value, ports) {
|
|
129
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
130
|
+
c.problem('services', 'must list the compose services `up` starts, dependencies first');
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return value.map((item, index) => {
|
|
134
|
+
const at = `services[${index}]`;
|
|
135
|
+
if (!closedFields(c, at, item, ['name', 'wait', 'port'], 'local'))
|
|
136
|
+
return { name: '', wait: 'started', port: null };
|
|
137
|
+
const entry = item;
|
|
138
|
+
patternField(c, `${at}.name`, entry.name, NAME);
|
|
139
|
+
const wait = String(entry.wait ?? 'healthy');
|
|
140
|
+
if (!['healthy', 'started', 'tcp'].includes(wait))
|
|
141
|
+
c.problem(`${at}.wait`, 'must be healthy (the compose healthcheck), tcp (the port accepts) or started');
|
|
142
|
+
const port = entry.port === undefined ? null : String(entry.port);
|
|
143
|
+
if (wait === 'tcp' && (port === null || !Object.hasOwn(ports, port)))
|
|
144
|
+
c.problem(`${at}.port`, 'must name a declared port when wait is tcp');
|
|
145
|
+
if (port !== null && !Object.hasOwn(ports, port))
|
|
146
|
+
c.problem(`${at}.port`, `${port} is not a declared port`);
|
|
147
|
+
return { name: String(entry.name ?? ''), wait: wait, port };
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function databaseOf(c, value, ports, services, role) {
|
|
151
|
+
if (value === undefined)
|
|
152
|
+
return null;
|
|
153
|
+
if (!closedFields(c, 'database', value, ['service', 'port', 'name', 'role', 'urlEnv', 'urlAs', 'adminUser', 'password', 'adminPassword', 'appRole', 'appPassword'], 'local'))
|
|
154
|
+
return null;
|
|
155
|
+
const entry = value;
|
|
156
|
+
patternField(c, 'database.service', entry.service, NAME);
|
|
157
|
+
if (!services.some((service) => service.name === entry.service))
|
|
158
|
+
c.problem('database.service', 'must be one of the started services');
|
|
159
|
+
if (!Object.hasOwn(ports, String(entry.port)))
|
|
160
|
+
c.problem('database.port', 'must name a declared port');
|
|
161
|
+
const own = entry.role === undefined ? role : String(entry.role);
|
|
162
|
+
patternField(c, 'database.role', own, SQL_NAME);
|
|
163
|
+
const name = entry.name === undefined ? own : String(entry.name);
|
|
164
|
+
patternField(c, 'database.name', name, SQL_NAME);
|
|
165
|
+
patternField(c, 'database.urlEnv', entry.urlEnv, KEY);
|
|
166
|
+
const urlAs = String(entry.urlAs ?? 'file');
|
|
167
|
+
if (!['env', 'file'].includes(urlAs))
|
|
168
|
+
c.problem('database.urlAs', 'must be env (the URL in the process environment) or file (<KEY>_FILE, preferred)');
|
|
169
|
+
const adminUser = String(entry.adminUser ?? 'postgres');
|
|
170
|
+
patternField(c, 'database.adminUser', adminUser, SQL_NAME);
|
|
171
|
+
if (own === adminUser || own === 'postgres')
|
|
172
|
+
c.problem('database.role', 'must be a role of this environment alone, never the superuser and never a role another environment uses');
|
|
173
|
+
const appRole = entry.appRole === undefined ? null : String(entry.appRole);
|
|
174
|
+
if (appRole !== null)
|
|
175
|
+
patternField(c, 'database.appRole', appRole, SQL_NAME);
|
|
176
|
+
if ((appRole === null) !== (entry.appPassword === undefined))
|
|
177
|
+
c.problem('database.appPassword', 'is declared with appRole, both or neither: the app role the migrations create gets its own password per environment');
|
|
178
|
+
if (appRole !== null && (appRole === own || appRole === adminUser))
|
|
179
|
+
c.problem('database.appRole', 'must be a role other than the owner role and the superuser');
|
|
180
|
+
return {
|
|
181
|
+
appRole, appPassword: entry.appPassword === undefined ? null : ref(c, 'database.appPassword', entry.appPassword),
|
|
182
|
+
service: String(entry.service), port: String(entry.port), name, role: own, urlEnv: String(entry.urlEnv), urlAs: urlAs, adminUser,
|
|
183
|
+
password: ref(c, 'database.password', entry.password), adminPassword: ref(c, 'database.adminPassword', entry.adminPassword),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function custodyOf(c, value) {
|
|
187
|
+
if (value === undefined)
|
|
188
|
+
return [];
|
|
189
|
+
if (!Array.isArray(value)) {
|
|
190
|
+
c.problem('custody', 'must be a list of generated dev values');
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return value.map((item, index) => {
|
|
194
|
+
const at = `custody[${index}]`;
|
|
195
|
+
if (!closedFields(c, at, item, ['member', 'key', 'kind', 'bytes', 'publicKey'], 'local'))
|
|
196
|
+
return { member: '', key: '', kind: 'hex', bytes: 32, publicKey: null };
|
|
197
|
+
const entry = item;
|
|
198
|
+
const base = ref(c, at, { member: entry.member, key: entry.key });
|
|
199
|
+
if (!GENERATORS.includes(String(entry.kind)))
|
|
200
|
+
c.problem(`${at}.kind`, `must be one of ${GENERATORS.join(', ')}`);
|
|
201
|
+
const bytes = entry.bytes === undefined ? 32 : entry.bytes;
|
|
202
|
+
if (!Number.isInteger(bytes) || bytes < 8 || bytes > 1024)
|
|
203
|
+
c.problem(`${at}.bytes`, 'must be a whole byte count from 8 to 1024');
|
|
204
|
+
if (entry.publicKey !== undefined) {
|
|
205
|
+
if (entry.kind === 'rsa-pem' || entry.kind === 'ec-pem' || entry.kind === 'ed25519-pem')
|
|
206
|
+
patternField(c, `${at}.publicKey`, entry.publicKey, KEY);
|
|
207
|
+
else
|
|
208
|
+
c.problem(`${at}.publicKey`, 'is only for kind rsa-pem, ec-pem or ed25519-pem');
|
|
209
|
+
}
|
|
210
|
+
return { ...base, kind: String(entry.kind), bytes: bytes, publicKey: text(entry.publicKey) ? entry.publicKey : null };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function listOf(c, at, value, each) {
|
|
214
|
+
if (value === undefined)
|
|
215
|
+
return [];
|
|
216
|
+
if (!Array.isArray(value)) {
|
|
217
|
+
c.problem(at, 'must be a list');
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
return value.map((item, index) => each(item, index));
|
|
221
|
+
}
|
|
222
|
+
function referencedPorts(c, block) {
|
|
223
|
+
const known = new Set(Object.keys(block.ports));
|
|
224
|
+
const check = (at, value) => {
|
|
225
|
+
for (const match of value.matchAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu))
|
|
226
|
+
if (!known.has(match[1]))
|
|
227
|
+
c.problem(at, `names the undeclared port ${match[1]}`);
|
|
228
|
+
};
|
|
229
|
+
for (const item of [...block.ready, ...block.verify])
|
|
230
|
+
check(`url of ${item.name}`, item.url);
|
|
231
|
+
for (const origin of block.origins)
|
|
232
|
+
check(`origin ${origin.name}`, origin.url);
|
|
233
|
+
for (const run of [...block.setup, ...block.seed, ...block.processes]) {
|
|
234
|
+
for (const part of run.argv)
|
|
235
|
+
check(`argv of ${run.name}`, part);
|
|
236
|
+
for (const value of Object.values(run.env))
|
|
237
|
+
check(`env of ${run.name}`, value);
|
|
238
|
+
}
|
|
239
|
+
for (const process of block.processes) {
|
|
240
|
+
if (process.port !== null && !known.has(process.port))
|
|
241
|
+
c.problem(`processes.${process.name}.port`, `${process.port} is not a declared port`);
|
|
242
|
+
if (process.health)
|
|
243
|
+
check(`health of ${process.name}`, process.health.url);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** The normalised `local` block, or the problems that stop it. `project` and `env` name the defaults (`<project>-<env>`, the role). */
|
|
247
|
+
export function normalizeLocal(raw, { project, env }) {
|
|
248
|
+
const c = collector();
|
|
249
|
+
if (!closedFields(c, 'local', raw, TOP, 'local'))
|
|
250
|
+
return { config: null, problems: c.problems };
|
|
251
|
+
const entry = raw;
|
|
252
|
+
const compose = String(entry.project ?? `${project}-${env}`).toLowerCase().replaceAll(/[^a-z0-9_-]+/gu, '-');
|
|
253
|
+
patternField(c, 'project', compose, PROJECT);
|
|
254
|
+
const ports = portsOf(c, entry.ports);
|
|
255
|
+
const services = servicesOf(c, entry.services, ports);
|
|
256
|
+
const role = `${project}_${env}`.toLowerCase().replaceAll(/[^a-z0-9]+/gu, '_').replace(/^[^a-z]+/u, 'a');
|
|
257
|
+
const names = new Set();
|
|
258
|
+
const block = {
|
|
259
|
+
project: compose, ports, services,
|
|
260
|
+
database: databaseOf(c, entry.database, ports, services, role),
|
|
261
|
+
composeEnv: listOf(c, 'composeEnv', entry.composeEnv, (item, index) => {
|
|
262
|
+
const at = `composeEnv[${index}]`;
|
|
263
|
+
if (!closedFields(c, at, item, ['name', 'member', 'key'], 'local'))
|
|
264
|
+
return { name: '', member: '', key: '' };
|
|
265
|
+
const value = item;
|
|
266
|
+
patternField(c, `${at}.name`, value.name, KEY);
|
|
267
|
+
return { name: String(value.name), ...ref(c, at, { member: value.member, key: value.key }) };
|
|
268
|
+
}),
|
|
269
|
+
grants: listOf(c, 'grants', entry.grants, (item, index) => {
|
|
270
|
+
const at = `grants[${index}]`;
|
|
271
|
+
if (!closedFields(c, at, item, ['name', 'keys'], 'local'))
|
|
272
|
+
return { name: '', keys: [] };
|
|
273
|
+
const value = item;
|
|
274
|
+
patternField(c, `${at}.name`, value.name, NAME);
|
|
275
|
+
const keys = strings(c, `${at}.keys`, value.keys, KEY);
|
|
276
|
+
if (keys.length === 0)
|
|
277
|
+
c.problem(`${at}.keys`, 'must list the keys the service reads');
|
|
278
|
+
return { name: String(value.name), keys };
|
|
279
|
+
}),
|
|
280
|
+
dirs: strings(c, 'dirs', entry.dirs, NAME_PATTERN),
|
|
281
|
+
custody: custodyOf(c, entry.custody),
|
|
282
|
+
setup: listOf(c, 'setup', entry.setup, (item, index) => command(c, `setup[${index}]`, item, COMMAND, names)),
|
|
283
|
+
seed: listOf(c, 'seed', entry.seed, (item, index) => command(c, `seed[${index}]`, item, SEED, names)),
|
|
284
|
+
processes: listOf(c, 'processes', entry.processes, (item, index) => {
|
|
285
|
+
const at = `processes[${index}]`;
|
|
286
|
+
const base = command(c, at, item, PROCESS, names);
|
|
287
|
+
const value = plain(item) ? item : {};
|
|
288
|
+
return { ...base, port: value.port === undefined ? null : String(value.port), health: value.health === undefined ? null : probe(c, `${at}.health`, { name: base.name || 'health', ...value.health }) };
|
|
289
|
+
}),
|
|
290
|
+
ready: listOf(c, 'ready', entry.ready, (item, index) => probe(c, `ready[${index}]`, item)),
|
|
291
|
+
verify: listOf(c, 'verify', entry.verify, (item, index) => probe(c, `verify[${index}]`, item)),
|
|
292
|
+
origins: listOf(c, 'origins', entry.origins, (item, index) => {
|
|
293
|
+
const at = `origins[${index}]`;
|
|
294
|
+
if (!closedFields(c, at, item, ['name', 'url'], 'local'))
|
|
295
|
+
return { name: '', url: '' };
|
|
296
|
+
const value = item;
|
|
297
|
+
patternField(c, `${at}.name`, value.name, NAME);
|
|
298
|
+
return { name: String(value.name), url: String(value.url ?? '') };
|
|
299
|
+
}),
|
|
300
|
+
resources: { diskGb: 10, ramMb: 4096 },
|
|
301
|
+
};
|
|
302
|
+
if (entry.resources !== undefined && closedFields(c, 'resources', entry.resources, ['diskGb', 'ramMb'], 'local')) {
|
|
303
|
+
const value = entry.resources;
|
|
304
|
+
for (const [key, floor] of [['diskGb', 1], ['ramMb', 512]]) {
|
|
305
|
+
if (value[key] === undefined)
|
|
306
|
+
continue;
|
|
307
|
+
if (!Number.isInteger(value[key]) || value[key] < floor)
|
|
308
|
+
c.problem(`resources.${key}`, `must be a whole number from ${floor}`);
|
|
309
|
+
else
|
|
310
|
+
block.resources[key] = value[key];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
referencedPorts(c, block);
|
|
314
|
+
const generated = new Set(block.custody.flatMap((item) => [`${item.member}/${item.key}`, ...(item.publicKey ? [`${item.member}/${item.publicKey}`] : [])]));
|
|
315
|
+
if (generated.size < block.custody.length)
|
|
316
|
+
c.problem('custody', 'declares a key twice');
|
|
317
|
+
return c.problems.length === 0 ? { config: block, problems: [] } : { config: null, problems: c.problems };
|
|
318
|
+
}
|
|
319
|
+
/** Replaces `${port.<name>}` with the chosen host port. */
|
|
320
|
+
export const expandPorts = (value, ports) => value.replaceAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu, (_whole, name) => String(ports[name] ?? ''));
|
|
@@ -535,7 +535,7 @@ const checkStacksManifest = (manifest, model, { stacksRoot, environment, root, r
|
|
|
535
535
|
const lite = repo.edition === 'lite';
|
|
536
536
|
checkStacksTree({ stacksRoot, manifest, environmentNames, add, lite });
|
|
537
537
|
checkKubernetesDeclaration(manifest, add);
|
|
538
|
-
for (const issue of swarmInventoryIssues({ components: manifest.components, lite }))
|
|
538
|
+
for (const issue of swarmInventoryIssues({ components: manifest.components, lite, swarm: new Set(Object.values(object(manifest.environments) ? manifest.environments : {}).filter((spec) => spec?.runtime === 'docker-swarm').flatMap((spec) => Object.keys(object(spec.components) ? spec.components : {}))) }))
|
|
539
539
|
add('HFS_STACKS_SHAPE', issue.at, issue.message);
|
|
540
540
|
checkSelectedEnvironment({ environment, environmentNames, environments, stacksRoot, ids, root, model, manifest, lite, repo, add });
|
|
541
541
|
};
|
|
@@ -10,9 +10,10 @@ export declare function composeIncludePaths(doc: Mapping): {
|
|
|
10
10
|
errors: string[];
|
|
11
11
|
};
|
|
12
12
|
/** Inventory rules are independent of a selected environment or rendered deployment model. */
|
|
13
|
-
export declare function swarmInventoryIssues({ components, lite }: {
|
|
13
|
+
export declare function swarmInventoryIssues({ components, lite, swarm }: {
|
|
14
14
|
components: unknown;
|
|
15
15
|
lite?: boolean;
|
|
16
|
+
swarm?: ReadonlySet<string>;
|
|
16
17
|
}): SwarmIssue[];
|
|
17
18
|
/** Validate the same deployment model in HFS and the deployment evidence gate. */
|
|
18
19
|
export declare function swarmModelIssues({ model, components, bindings, lite }: {
|
|
@@ -49,12 +49,12 @@ function jobDeclarationIssues(id, component, components, add) {
|
|
|
49
49
|
add(`components.${id}.schedule`, 'replicated-job must declare its cron schedule');
|
|
50
50
|
}
|
|
51
51
|
/** Inventory rules are independent of a selected environment or rendered deployment model. */
|
|
52
|
-
export function swarmInventoryIssues({ components, lite = false }) {
|
|
52
|
+
export function swarmInventoryIssues({ components, lite = false, swarm }) {
|
|
53
53
|
const errors = [], catalog = mapping(components);
|
|
54
54
|
const add = (at, message) => { errors.push({ at, message }); };
|
|
55
55
|
for (const [id, entry] of Object.entries(catalog)) {
|
|
56
56
|
const component = mapping(entry);
|
|
57
|
-
if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
|
|
57
|
+
if (lite && (swarm === undefined || swarm.has(id)) && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
|
|
58
58
|
add(`components.${id}.image`, 'lite runs no backing image; its db component runs the image .starcistacks/images/db/Dockerfile builds, storage is the external S3 endpoint, identity the API');
|
|
59
59
|
jobDeclarationIssues(id, component, catalog, add);
|
|
60
60
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** What a module verb answers: every app-relative path it wrote, rewrote or deleted. */
|
|
2
|
+
export interface ModuleResult {
|
|
3
|
+
readonly created: string[];
|
|
4
|
+
readonly updated: string[];
|
|
5
|
+
readonly removed: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Adds the module package `name`, registers it and re-renders the registry. Refused when the app has no agent SDK package or the module exists. */
|
|
8
|
+
export declare function addModule({ root, name }: {
|
|
9
|
+
root: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}): ModuleResult;
|
|
12
|
+
/** Removes the module package `name`, its registrations and its entry in the registry; the last module takes the registry package with it. */
|
|
13
|
+
export declare function removeModule({ root, name }: {
|
|
14
|
+
root: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}): ModuleResult;
|