@starci/hfs 4.10.1 → 4.11.8
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 +76 -0
- package/README.md +40 -1
- package/db/drift.mjs +19 -16
- package/db/image.mjs +13 -0
- package/db/migrate.mjs +3 -3
- package/db/postgres.mjs +1 -1
- package/package.json +2 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +23 -8
- package/runtime/knowledge/hfs/rules.yaml +54 -13
- package/runtime/knowledge/hfs/slots.yaml +67 -36
- package/runtime/knowledge/patterns/be/event-bus.yaml +12 -10
- package/runtime/knowledge/patterns/be/idempotency.yaml +1 -1
- package/runtime/knowledge/patterns/be/jobs.yaml +3 -0
- package/runtime/knowledge/patterns/be/queues.yaml +14 -14
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +28 -18
- package/runtime/modules/schemas/application-stacks.schema.yaml +4 -2
- package/runtime/scripts/gates/starcistacks.mjs +6 -2
- package/runtime/scripts/guards/push-scan.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +3 -2
- package/runtime/scripts/hfs/db-baseline-verb.mjs +94 -22
- package/runtime/scripts/hfs/db-errors.mjs +2 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -5
- package/runtime/scripts/hfs/db-squash.mjs +514 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -15
- package/runtime/scripts/hfs/example-secrets.mjs +4 -3
- package/runtime/scripts/hfs/rule-params-shape.mjs +11 -9
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-config.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +16 -1
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +2 -110
- package/runtime/scripts/hfs/rules/database-queues.mjs +53 -239
- package/runtime/scripts/hfs/rules/database-sql.mjs +2 -0
- package/runtime/scripts/hfs/rules/database-transaction.mjs +45 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +1 -1
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/env-parity.mjs +1 -1
- package/runtime/scripts/hfs/rules/lite-secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -1
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +2 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +2 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +3 -4
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/sealed-members.mjs +10 -6
- package/runtime/scripts/hfs/secret-files.mjs +16 -0
- package/runtime/scripts/hfs/secret-init.mjs +120 -21
- package/runtime/scripts/hfs/secret-output.mjs +2 -1
- package/runtime/scripts/hfs/secret-prompt.mjs +128 -0
- package/runtime/scripts/hfs/secret-sops.mjs +93 -9
- package/runtime/scripts/hfs/secret-standard.mjs +315 -0
- package/runtime/scripts/hfs/secret-store.mjs +23 -9
- package/runtime/scripts/hfs/secret.mjs +110 -46
- package/runtime/scripts/hfs/slots.mjs +8 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +20 -6
- package/runtime/scripts/hfs/stacks-gate.mjs +9 -2
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +1 -1
- package/runtime/scripts/lib/sealed-member.mjs +36 -0
- package/runtime/scripts/lib/secret-patterns.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +4 -6
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-queue-lite.mjs +24 -5
- package/scaffold/app.mjs +2 -2
- package/src/main.mjs +5 -4
- package/stack/build.mjs +3 -1
- package/stack/config.mjs +2 -1
- package/stack/docker-transport.mjs +2 -1
- package/stack/remote.mjs +4 -3
- package/stack/sealed.mjs +9 -8
- package/stack/target-binding.mjs +2 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +47 -12
- package/templates/app/db-bootstrap/db/bootstrap.sql +9 -14
- package/templates/app/db-image/starcistacks/images/db/Dockerfile +18 -0
- package/templates/app/gitignore +2 -1
- package/templates/app/gitignore-lite +3 -1
- package/templates/app/package-scripts/db.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +4 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/README.md +2 -2
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +7 -5
- package/templates/be/inbox/events.json.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +58 -11
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +13 -1
- package/templates/be/inbox/platform/event-bus/event-bus.log-events.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +11 -5
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +9 -2
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +18 -5
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +37 -16
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +33 -5
- package/templates/be/inbox/platform/event-bus/event-consumer.service.spec.ts.tpl +143 -0
- package/templates/be/inbox/platform/event-bus/event-consumer.service.ts.tpl +119 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +13 -4
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +4 -4
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +4 -4
- package/templates/be/inbox/platform/migration.sql.tpl +97 -34
- package/templates/be/inbox/service.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +4 -4
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +5 -5
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.spec.ts.tpl +65 -0
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.ts.tpl +78 -0
- package/templates/be/patterns/jobs/platform-lite/job-ticks.contracts.ts.tpl +16 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.module.ts.tpl +40 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +2 -2
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +37 -2
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +36 -3
- package/templates/be/skeleton/src/modules/platform/http/http.config.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +5 -3
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +9 -2
- package/templates/be/skeleton/src/modules/platform/http/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.spec.ts +96 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.ts +93 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -1
- package/runtime/scripts/hfs/rules/database-queue-outbox.mjs +0 -102
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +0 -24
package/src/main.mjs
CHANGED
|
@@ -108,7 +108,7 @@ starci app codemod button-intent [--write] [--format json|markdown] [--report <f
|
|
|
108
108
|
starci app sync (--check | --write) [--cwd <dir>]
|
|
109
109
|
starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--wiring] [--coverage-from <lcov>]
|
|
110
110
|
starci app db new <kebab-name> [--json] [--cwd <dir>]
|
|
111
|
-
starci app db baseline [--stamp <version>] [--url-env <NAME>] [--apply] [--cwd <dir>]
|
|
111
|
+
starci app db baseline [--stamp <version>] [--url-env <NAME>] [--squash [--drop-extension <a,b>] [--drop-pgmq-queue <q,job_*>] [--seed-tables <t1,t2>] [--image <ref>]] [--apply] [--cwd <dir>]
|
|
112
112
|
starci app db restamp --map <restamp.yaml> [--url-env <NAME>] [--apply] [--cwd <dir>]
|
|
113
113
|
starci app db plan [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
114
114
|
starci app db dump [--url-env <NAME>] --out <dir> [--schema-only] [--json] [--cwd <dir>]
|
|
@@ -460,11 +460,12 @@ function errorMessage(error) {
|
|
|
460
460
|
/**
|
|
461
461
|
* `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
|
|
462
462
|
* scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the database runner, `runLinter` the linter transport, `checkApp`
|
|
463
|
-
* the HFS check and `stackIo` Swarm tools; production defaults execute every check.
|
|
463
|
+
* the HFS check and `stackIo` Swarm tools; production defaults execute every check. `hostIdentity` ({ label, read }) is the
|
|
464
|
+
* harness CLI's own secret.env SOPS_AGE_KEY, which `secret` reads last for the age identity; `prompt` replaces its masked prompt.
|
|
464
465
|
*/
|
|
465
466
|
export async function main(argv, {
|
|
466
467
|
cwd: baseCwd = process.cwd(), harness, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
|
|
467
|
-
env = process.env, stdin, sops, tools, platform, stackIo, runLinter, checkApp,
|
|
468
|
+
env = process.env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp,
|
|
468
469
|
} = {}) {
|
|
469
470
|
try {
|
|
470
471
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
@@ -472,7 +473,7 @@ export async function main(argv, {
|
|
|
472
473
|
const out = global.quiet ? () => {} : stdout;
|
|
473
474
|
if (global.help) { out(USAGE); return 0; }
|
|
474
475
|
if (!VERBS.includes(verb)) { stderr(USAGE); return 2; }
|
|
475
|
-
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
476
|
+
return await dispatchMain(verb, rest, { cwd: global.cwd, harness, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, prompt, hostIdentity, sops, tools, platform, stackIo, runLinter, checkApp });
|
|
476
477
|
} catch (error) {
|
|
477
478
|
stderr(errorMessage(error));
|
|
478
479
|
return 2;
|
package/stack/build.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { buildArgs } from './build-args.mjs';
|
|
|
13
13
|
import { firstLine, lines as splitLines, tool } from './context.mjs';
|
|
14
14
|
import { failure, refusal } from './errors.mjs';
|
|
15
15
|
import { requireAgeIdentity } from './sealed.mjs';
|
|
16
|
+
import { DB_DOCKERFILE, DB_IMAGE_APP } from '../db/image.mjs';
|
|
16
17
|
import { scanImage } from './image-scan.mjs';
|
|
17
18
|
|
|
18
19
|
const RELEASE_TAG = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
@@ -44,8 +45,9 @@ function ownerOf(ctx, config) {
|
|
|
44
45
|
return match[1].toLowerCase();
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/** The Dockerfile of an app: be/apps/<app>/Dockerfile or fe/apps/<app>/Dockerfile; ambiguity and absence are refusals. */
|
|
48
|
+
/** The Dockerfile of an app: be/apps/<app>/Dockerfile or fe/apps/<app>/Dockerfile, or the managed .starcistacks/images/db/Dockerfile for the db image; ambiguity and absence are refusals. */
|
|
48
49
|
function dockerfileOf(config, app) {
|
|
50
|
+
if (app === DB_IMAGE_APP && fs.existsSync(path.join(config.repoRoot, DB_DOCKERFILE))) return DB_DOCKERFILE;
|
|
49
51
|
const found = SIDES.map((side) => `${side}/apps/${app}/Dockerfile`).filter((file) => fs.existsSync(path.join(config.repoRoot, file)));
|
|
50
52
|
if (found.length === 0) throw refusal('STACK_BUILD_NO_DOCKERFILE', `no Dockerfile for app ${app} (be/apps/${app}/Dockerfile or fe/apps/${app}/Dockerfile)`);
|
|
51
53
|
if (found.length > 1) throw refusal('STACK_BUILD_AMBIGUOUS', `app ${app} has a Dockerfile on both sides: ${found.join(', ')}`);
|
package/stack/config.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// config.mjs - loads `environments.<env>.deploy` of .starcistacks/application-stacks.yaml for the stack verbs.
|
|
2
2
|
// The block is read and judged by the same module `starci app check` uses (runtime/scripts/hfs/stack-deploy-config.mjs).
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import { isSealedName, memberOfName } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
6
7
|
import { DEPLOY_CONFIG_CODE, NAME, normalizeDeploy } from '../runtime/scripts/hfs/stack-deploy-config.mjs';
|
|
@@ -22,7 +23,7 @@ const projectOf = (repoRoot) => {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const sealedMembers = (secretsDir) => {
|
|
25
|
-
try { return new Set(fs.readdirSync(secretsDir).filter(
|
|
26
|
+
try { return new Set(fs.readdirSync(secretsDir).filter(isSealedName).map(memberOfName)); } catch { return new Set(); }
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const stackServices = (stackFile) => {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// an explicit environment are affected (the minimal environment of `docker stack config` stays exactly as the render built it). The key lives in
|
|
5
5
|
// a private temp directory that is overwritten and removed when the verb ends; it is never in argv, a line or an error.
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import { memberFile } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import { writePrivate } from './private-file.mjs';
|
|
9
10
|
import { openSealedSession } from './remote.mjs';
|
|
@@ -21,7 +22,7 @@ function needed(ctx, config, sshCmd) {
|
|
|
21
22
|
if (sshCmd || ctx.env.DOCKER_HOST || ctx.env.GITHUB_ACTIONS === 'true' || ctx.env.STARCI_STACK_SEALED_SSH === '0') return false;
|
|
22
23
|
const { sealed } = config.remote;
|
|
23
24
|
if (!sealed) return false;
|
|
24
|
-
return [sealed.ops, sealed.sshKey, sealed.knownHosts].every((slug) => fs.existsSync(
|
|
25
|
+
return [sealed.ops, sealed.sshKey, sealed.knownHosts].every((slug) => fs.existsSync(memberFile(config.secretsDir, slug)));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function sshConfig(session, controlPath, windows) {
|
package/stack/remote.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// `bash -s` fail with `set: pipefail`). Key material lives in memory and in a private temp directory only; it is never in argv (only the key
|
|
9
9
|
// FILE path is), in a line, in a result or in an error message.
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
+
import { isLegacyName, memberFile } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
11
12
|
import os from 'node:os';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { tool } from './context.mjs';
|
|
@@ -78,7 +79,7 @@ function openMember(slug, read) {
|
|
|
78
79
|
|
|
79
80
|
function opsAccount(ctx, config) {
|
|
80
81
|
const slug = config.remote.sealed.ops;
|
|
81
|
-
const map = openMember(slug, () => ctx.sops.decrypt(
|
|
82
|
+
const map = openMember(slug, () => ctx.sops.decrypt(memberFile(config.secretsDir, slug), isLegacyName(memberFile(config.secretsDir, slug)) ? 'dotenv' : 'yaml'));
|
|
82
83
|
for (const name of ['VPS_HOST', 'VPS_USER']) {
|
|
83
84
|
const value = map[name];
|
|
84
85
|
if (typeof value !== 'string' || !ACCOUNT.test(value) || value.startsWith('-')) throw refusal('STACK_REMOTE_OPS', `${slug} must hold ${name} (letters, digits, . _ @ -)`);
|
|
@@ -87,7 +88,7 @@ function opsAccount(ctx, config) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function binaryMember(ctx, config, slug, names) {
|
|
90
|
-
return unwrap(openMember(slug, () => ctx.sops.decryptBinary(
|
|
91
|
+
return unwrap(openMember(slug, () => ctx.sops.decryptBinary(memberFile(config.secretsDir, slug))), names);
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
const withNewline = (text) => `${text.trim()}\n`;
|
|
@@ -159,7 +160,7 @@ function withOwnerTransport(ctx, config, { sshCmd = null } = {}) {
|
|
|
159
160
|
const { sealed } = config.remote;
|
|
160
161
|
if (config.nativeTarget) return Object.create(ctx, { env: { value: { ...ctx.env, STARCI_STACK_SEALED_SSH: '1' }, enumerable: true } });
|
|
161
162
|
if (sshCmd || !sealed || ctx.env.GITHUB_ACTIONS === 'true' || ctx.env.STARCI_STACK_SEALED_SSH !== undefined) return ctx;
|
|
162
|
-
const present = [sealed.ops, sealed.sshKey, sealed.knownHosts].every((slug) => fs.existsSync(
|
|
163
|
+
const present = [sealed.ops, sealed.sshKey, sealed.knownHosts].every((slug) => fs.existsSync(memberFile(config.secretsDir, slug)));
|
|
163
164
|
if (!present) return ctx;
|
|
164
165
|
return Object.create(ctx, { env: { value: { ...ctx.env, STARCI_STACK_SEALED_SSH: '1' }, enumerable: true } });
|
|
165
166
|
}
|
package/stack/sealed.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// service is granted (the shared half of the release tools). No value is ever written to disk, printed or put in an argument list:
|
|
3
3
|
// callers receive `value` and must keep it in memory. Config objects (non-secret files mounted into services) are versioned by content.
|
|
4
4
|
import { createHash, createHmac } from 'node:crypto';
|
|
5
|
+
import { memberFile } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { secretRoots } from '../runtime/scripts/hfs/secret-files.mjs';
|
|
@@ -65,10 +66,10 @@ export function requireAgeIdentity(ctx, config, purpose) {
|
|
|
65
66
|
if (!hasAgeIdentity(ctx, config)) throw refusal('STACK_AGE_IDENTITY_MISSING', `${purpose} needs the sealed members, and no age identity is available: ${AGE_HINT}`);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/** Opens one sealed member (`<secretsDir>/<member>.enc`) into a { KEY: value } map. Names the member only on failure. */
|
|
69
|
+
/** Opens one sealed member (`<secretsDir>/<member>.enc.yaml`, or a legacy `.enc`) into a { KEY: value } map. Names the member only on failure. */
|
|
69
70
|
export function openMember(ctx, config, member) {
|
|
70
71
|
try {
|
|
71
|
-
const map = readMap(
|
|
72
|
+
const map = readMap(memberFile(config.secretsDir, member), ctx.sops);
|
|
72
73
|
return Object.fromEntries(Object.entries(map).map(([key, value]) => [key, typeof value === 'string' && !value.includes(String.fromCodePoint(0)) ? lfText(value) : value]));
|
|
73
74
|
} catch {
|
|
74
75
|
throw refusal('STACK_SECRET_MEMBER_UNREADABLE', `the sealed member ${member} cannot be opened${hasAgeIdentity(ctx, config) ? ' (is the age identity a recipient of this member?)' : `: no age identity is available, ${AGE_HINT}`}`);
|
|
@@ -116,13 +117,13 @@ function sealGrants(config, memberOf, versionKey) {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function sealSettings(config, memberOf) {
|
|
119
|
-
const block = config.secrets.settings;
|
|
120
|
-
if (block === null) return {};
|
|
121
|
-
const map = memberOf(block.member);
|
|
122
120
|
const settings = {};
|
|
123
|
-
for (const
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
for (const block of config.secrets.settings) {
|
|
122
|
+
const map = memberOf(block.member);
|
|
123
|
+
for (const key of block.keys.filter((name) => present(map, name))) {
|
|
124
|
+
if (UNSAFE_SETTING.test(map[key])) throw refusal('STACK_SETTING_INVALID', `${key} must be a plain one-line value`);
|
|
125
|
+
settings[key] = map[key];
|
|
126
|
+
}
|
|
126
127
|
}
|
|
127
128
|
return settings;
|
|
128
129
|
}
|
package/stack/target-binding.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Public endpoint assertions for admitted native consumers; this module never opens custody.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { memberFile } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { refusal } from './errors.mjs';
|
|
5
6
|
|
|
@@ -52,7 +53,7 @@ export function bindStackTarget(ctx, config, binding) {
|
|
|
52
53
|
|| !config.remote.sealed || ctx.env.DOCKER_HOST || ctx.env.DOCKER_CONTEXT || ctx.env.GITHUB_ACTIONS
|
|
53
54
|
|| ctx.env.STARCI_STACK_SEALED_SSH !== undefined) denied();
|
|
54
55
|
for (const member of Object.values(config.remote.sealed)) {
|
|
55
|
-
const file =
|
|
56
|
+
const file = memberFile(config.secretsDir, member);
|
|
56
57
|
assertBoundCustody(config.repoRoot, file);
|
|
57
58
|
}
|
|
58
59
|
return { ...config, nativeTarget: binding };
|
package/sync/hygiene.mjs
CHANGED
|
@@ -58,7 +58,7 @@ export function hygieneFindings(files, ignored) {
|
|
|
58
58
|
if (file.startsWith(WORK) && ignored.has(file)) {
|
|
59
59
|
findings.push({ file, code: 'HFS_WORK_AGENT_DATA', message: 'is agent output, which .starciwork/.gitignore refuses; keep it in the scratchpad or the blob store' });
|
|
60
60
|
}
|
|
61
|
-
if (file.startsWith(STACKS) &&
|
|
61
|
+
if (file.startsWith(STACKS) && !/\.enc(?:\.yaml)?$/.test(file) && !file.endsWith('.env.example') && !file.endsWith('/secrets/.gitkeep')) {
|
|
62
62
|
if (file.includes('/secrets/')) findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'sits under secrets/ but is not sealed; only <slug>.enc may be tracked' });
|
|
63
63
|
else if (PLAINTEXT_NAME.test(file)) findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'is a plaintext secret; seal it to .starcistacks/<env>/secrets/<slug>.enc' });
|
|
64
64
|
}
|
package/sync/index.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs
|
|
|
30
30
|
import { isFile } from '../runtime/scripts/lib/fs-kind.mjs';
|
|
31
31
|
import { readDeclaredSonarKey } from './sonar-key.mjs';
|
|
32
32
|
import { readDeclaredStack, stackWorkflowVariables } from './stack-images.mjs';
|
|
33
|
+
import { DB_DOCKERFILE, DB_IMAGE_APP, RETIRED_DB_DOCKERFILE } from '../db/image.mjs';
|
|
33
34
|
import { byCodeUnit } from '../report/order.mjs';
|
|
34
35
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
35
36
|
import { ciWorkflowCommands } from './ci-commands.mjs';
|
|
@@ -39,6 +40,7 @@ import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
|
|
|
39
40
|
import { LIST } from '../runtime/scripts/lib/loosening-config.mjs';
|
|
40
41
|
import { readTruths } from '../runtime/scripts/lib/loosening-state.mjs';
|
|
41
42
|
import { parseYaml as bundledParseYaml } from '../runtime/engine/yaml.mjs';
|
|
43
|
+
import { bootstrapSection } from '../runtime/scripts/hfs/rules/database-constants.mjs';
|
|
42
44
|
|
|
43
45
|
export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
|
|
44
46
|
const NODE_MAJOR = 22;
|
|
@@ -57,9 +59,10 @@ export class SyncError extends Error {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// mode: file = whole file, block = the marked block inside a file the app also writes to, scripts = the `scripts` block of
|
|
60
|
-
// package.json
|
|
61
|
-
//
|
|
62
|
-
|
|
62
|
+
// package.json, section = the whole file but the one marked section a database verb owns (db/bootstrap.sql, whose squash section
|
|
63
|
+
// `starci app db baseline --squash` writes). A template that can hold a comment opens with {{header}}; a file whose exact content is
|
|
64
|
+
// specified (JSON, the eslint one-liner) has no header.
|
|
65
|
+
const MODE_OF_GROUP = Object.freeze({ 'package-scripts': 'scripts', 'package-scripts-lite': 'scripts', 'db-bootstrap': 'section' });
|
|
63
66
|
const GLOB_CHARACTERS = new Set('*?<>{}[]');
|
|
64
67
|
const literalFile = file => file.length >= 2 && !file.endsWith('/') && ![...file].some(character => GLOB_CHARACTERS.has(character));
|
|
65
68
|
|
|
@@ -156,9 +159,25 @@ function trailingNewline(text) {
|
|
|
156
159
|
return text.slice(0, end) + '\n';
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The base image of a lite app's managed .starcistacks/images/db/Dockerfile (contexts/stack/pins.yaml images postgres, ruling:lite-official-postgres-image): the
|
|
164
|
+
* Docker Official postgres image by digest, to which the Dockerfile adds PGVECTOR_PACKAGE from PGDG and the PGMQ release.
|
|
165
|
+
*/
|
|
166
|
+
export const DB_BASE_IMAGE = 'postgres:17.11@sha256:31ba9c94a03ada9988f2bb493a22a75eef3a185884533e5cdbcd498ea835b08f';
|
|
167
|
+
/** The PGDG package of pgvector at the exact version the db image installs (the one extension the image does not ship). */
|
|
168
|
+
export const PGVECTOR_PACKAGE = 'postgresql-17-pgvector=0.8.7-1.pgdg13+1';
|
|
169
|
+
/**
|
|
170
|
+
* The pgmq release the db image installs for the event bus (ruling:lite-pgmq-bus-bullmq-jobs): its source tarball, verified by sha256
|
|
171
|
+
* when Docker adds it. PGDG does not package pgmq; it is SQL objects only, so the Dockerfile copies its control and script files.
|
|
172
|
+
*/
|
|
173
|
+
export const PGMQ = Object.freeze({
|
|
174
|
+
version: '1.13.1',
|
|
175
|
+
source: 'https://github.com/pgmq/pgmq/archive/refs/tags/v1.13.1.tar.gz',
|
|
176
|
+
sha256: '67221f288f42f95c002a64df40f7d660e765e32df8ada0f2672ba915a14827f7',
|
|
177
|
+
});
|
|
178
|
+
/** The local tag the development db container and the drift scratch database run, built from the app's .starcistacks/images/db/Dockerfile. */
|
|
179
|
+
export const dbLocalImage = project => `${project}-db:local`;
|
|
159
180
|
/** The digest-pinned image of a lite app's declared Redis, in every environment that runs it (ruling:R212/case-10). */
|
|
160
|
-
/** The pinned image of a lite app's db service (contexts/stack/pins.yaml images): PostgreSQL with pg_cron, pg_net, pgmq and pgvector. */
|
|
161
|
-
export const DB_IMAGE = 'supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2';
|
|
162
181
|
export const REDIS_IMAGE = 'redis:7@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f';
|
|
163
182
|
|
|
164
183
|
const redisDevText = project => render(readBundled('app/package-scripts/redis-dev.json'), { project, redisImage: REDIS_IMAGE });
|
|
@@ -322,17 +341,19 @@ function imageEntries(app, source) {
|
|
|
322
341
|
}
|
|
323
342
|
|
|
324
343
|
/**
|
|
325
|
-
* The build matrix of the images workflow: one entry per declared app (be then fe), each with the image name and its Dockerfile. The workflow
|
|
344
|
+
* The build matrix of the images workflow: one entry per declared app (be then fe, then a lite app's db image), each with the image name and its Dockerfile. The workflow
|
|
326
345
|
* runs on the release tag, so every image is built there; no path filter narrows it. A YAML block mapping per entry, indented for
|
|
327
346
|
* `strategy.matrix.include`.
|
|
328
347
|
*/
|
|
329
348
|
export function imageMatrix(app, source) {
|
|
330
349
|
const entry = (side, item) => ` - name: ${item.name}\n file: ${dockerfilePath(side, item.name)}`;
|
|
331
|
-
|
|
350
|
+
const db = app.edition === 'lite' ? [` - name: ${DB_IMAGE_APP}\n file: ${DB_DOCKERFILE}`] : [];
|
|
351
|
+
return [...imageEntries(app, source).map(image => entry(image.side, image.entry)), ...db].join('\n');
|
|
332
352
|
}
|
|
333
353
|
|
|
334
|
-
/** The Dockerfile of the app a deploy image names (be first, then fe); an app hfs.json does not declare cannot be built by the managed workflow. */
|
|
354
|
+
/** The Dockerfile of the app a deploy image names (be first, then fe, then a lite app's db image); an app hfs.json does not declare cannot be built by the managed workflow. */
|
|
335
355
|
function stackDockerfile(app, name) {
|
|
356
|
+
if (app.edition === 'lite' && name === DB_IMAGE_APP) return DB_DOCKERFILE;
|
|
336
357
|
const side = ['be', 'fe'].find(which => app.sides[which].apps.some(entry => entry.name === name));
|
|
337
358
|
if (!side) throw new SyncError('HFS_SYNC_STACK_IMAGE', `deploy.images names the app ${name}, which hfs.json does not declare under be or fe`);
|
|
338
359
|
return dockerfilePath(side, name);
|
|
@@ -375,8 +396,13 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
375
396
|
project: app.project,
|
|
376
397
|
appBinary: APP_BINARY,
|
|
377
398
|
appScripts: appScripts(app, source),
|
|
378
|
-
// A lite app's own database runs in development as one container of the
|
|
379
|
-
dbImage:
|
|
399
|
+
// A lite app's own database runs in development as one container of the image its .starcistacks/images/db/Dockerfile builds (ruling:lite-db-image-in-stacks).
|
|
400
|
+
dbImage: dbLocalImage(app.project),
|
|
401
|
+
dbBaseImage: DB_BASE_IMAGE,
|
|
402
|
+
pgvectorPackage: PGVECTOR_PACKAGE,
|
|
403
|
+
pgmqVersion: PGMQ.version,
|
|
404
|
+
pgmqSource: PGMQ.source,
|
|
405
|
+
pgmqSha256: PGMQ.sha256,
|
|
380
406
|
// A lite app's declared Redis (sides.be.backing, ruling:R212/case-10) runs in development as one container of its pinned image.
|
|
381
407
|
backingScripts: lite && (app.sides.be.backing ?? []).includes('redis') ? redisDevText(app.project) : '',
|
|
382
408
|
// turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
|
|
@@ -460,9 +486,10 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
460
486
|
});
|
|
461
487
|
}
|
|
462
488
|
|
|
463
|
-
// The part of a file sync manages: all of it, the marked block, or the scripts block as text.
|
|
489
|
+
// The part of a file sync manages: all of it, all of it but the verb's section, the marked block, or the scripts block as text.
|
|
464
490
|
function managedPart(mode, text, markers = { begin: BLOCK_BEGIN, end: BLOCK_END }) {
|
|
465
491
|
if (mode === 'file') return text;
|
|
492
|
+
if (mode === 'section') return bootstrapSection(text).head;
|
|
466
493
|
if (mode === 'scripts') {
|
|
467
494
|
const scripts = parseScripts(text);
|
|
468
495
|
return scripts ? scriptsText(scripts) : null;
|
|
@@ -512,6 +539,7 @@ export function writtenContent(target, existing) {
|
|
|
512
539
|
}
|
|
513
540
|
return `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
|
|
514
541
|
}
|
|
542
|
+
if (target.mode === 'section') return `${target.content}${bootstrapSection(existing ?? '').section ?? ''}`;
|
|
515
543
|
if (target.mode === 'block' && existing) {
|
|
516
544
|
const current = managedPart('block', existing, target.markers);
|
|
517
545
|
if (current !== null) return existing.replace(current, () => target.content);
|
|
@@ -559,6 +587,12 @@ export async function renderRepo(root, { presets, parseYaml, notices, manifest =
|
|
|
559
587
|
|
|
560
588
|
/** The folders a lite app's sync retires (ruling lite-self-hosted-postgres-passport-minio): the generated Supabase row types nothing reads any more. */
|
|
561
589
|
const LITE_RETIRED_DIRS = Object.freeze(['supabase/types']);
|
|
590
|
+
/** The files a lite app's sync retires, each with the reason --check prints. */
|
|
591
|
+
const LITE_RETIRED_FILES = Object.freeze({
|
|
592
|
+
[RETIRED_DB_DOCKERFILE]: `the db image moved to ${DB_DOCKERFILE} (ruling lite-db-image-in-stacks)`,
|
|
593
|
+
});
|
|
594
|
+
/** Why a retired file goes: a retired file's own reason, else the folder's. */
|
|
595
|
+
const retiredReason = (file) => LITE_RETIRED_FILES[file] ?? 'the lite edition generates no database types';
|
|
562
596
|
|
|
563
597
|
/** The app-relative files of the retired folders that still exist under `root` of a lite app; sync --write deletes them, --check reports them. */
|
|
564
598
|
export function retiredFiles(root, hfs) {
|
|
@@ -572,6 +606,7 @@ export function retiredFiles(root, hfs) {
|
|
|
572
606
|
else found.push(rel);
|
|
573
607
|
};
|
|
574
608
|
for (const dir of LITE_RETIRED_DIRS) walk(dir);
|
|
609
|
+
for (const file of Object.keys(LITE_RETIRED_FILES)) walk(file);
|
|
575
610
|
return found;
|
|
576
611
|
}
|
|
577
612
|
|
|
@@ -590,7 +625,7 @@ function syncOptions(argv) {
|
|
|
590
625
|
function reportSyncCheck(root, targets, out, retired = []) {
|
|
591
626
|
const results = checkTargets(root, targets);
|
|
592
627
|
const bad = results.filter(item => item.status !== 'ok');
|
|
593
|
-
for (const file of retired) out(`HFS_SYNC_DRIFT ${file}: retired,
|
|
628
|
+
for (const file of retired) out(`HFS_SYNC_DRIFT ${file}: retired, ${retiredReason(file)}; run "npx starci app sync --write" to delete it`);
|
|
594
629
|
for (const result of bad) {
|
|
595
630
|
const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
|
|
596
631
|
const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
-- Managed by starci app sync (slot app.db.bootstrap); do not edit: run "npx starci app sync --write".
|
|
2
|
-
-- The pre-migration bootstrap of a lite app's self-hosted PostgreSQL (ruling lite-self-hosted-postgres-passport-minio)
|
|
3
|
-
--
|
|
4
|
-
-- auth.sessions, auth.uid/jwt/role) and the `storage`
|
|
5
|
-
--
|
|
6
|
-
-- of integrations/object-storage. This file gives the migrations those objects, EMPTY, and
|
|
7
|
-
--
|
|
2
|
+
-- The pre-migration bootstrap of a lite app's self-hosted PostgreSQL (ruling lite-self-hosted-postgres-passport-minio), run on the official
|
|
3
|
+
-- postgres image of .starcistacks/images/db/Dockerfile (ruling lite-official-postgres-image). Migrations written for Supabase reference objects GoTrue and Supabase
|
|
4
|
+
-- Storage created: the browser roles, the `auth` schema (auth.users, auth.identities, auth.sessions, auth.uid/jwt/role) and the `storage`
|
|
5
|
+
-- schema (storage.buckets, storage.objects). A lite app runs neither GoTrue nor storage-api: auth is the app's own (integrations/passport) and
|
|
6
|
+
-- storage is the S3 endpoint of integrations/object-storage. This file gives the migrations those objects, EMPTY, and the `extensions` schema
|
|
7
|
+
-- every allowed extension is created in (`create extension ... with schema extensions`, the app role's search_path is `public, extensions`);
|
|
8
|
+
-- nothing else of Supabase (supabase_admin, the realtime publication) exists. It is idempotent; `starci app db migrate` applies it as the
|
|
9
|
+
-- database owner before the first migration of an empty database.
|
|
8
10
|
--
|
|
9
11
|
-- auth.users keeps the GoTrue column set so restored rows and foreign keys from public tables stay valid; encrypted_password holds bcrypt hashes.
|
|
10
12
|
-- auth.uid(), auth.role() and auth.jwt() read the request.jwt.claims setting; the API connects as app_be and sets none, so they return NULL and
|
|
@@ -25,7 +27,7 @@ $roles$;
|
|
|
25
27
|
grant usage on schema auth, storage to anon, authenticated, service_role;
|
|
26
28
|
grant usage on schema extensions to anon, authenticated, service_role;
|
|
27
29
|
|
|
28
|
-
-- auth.users: the full GoTrue column set (
|
|
30
|
+
-- auth.users: the full GoTrue column set (a copy an earlier database carries is extended in place).
|
|
29
31
|
create table if not exists auth.users (id uuid primary key);
|
|
30
32
|
alter table auth.users
|
|
31
33
|
add column if not exists instance_id uuid,
|
|
@@ -130,10 +132,3 @@ alter table storage.buckets enable row level security;
|
|
|
130
132
|
|
|
131
133
|
grant select, insert, update, delete on storage.objects, storage.buckets to authenticated, service_role;
|
|
132
134
|
grant select on storage.objects, storage.buckets to anon;
|
|
133
|
-
|
|
134
|
-
-- Replication publication the baseline adds tables to (the image ships it; a bare PostgreSQL does not).
|
|
135
|
-
do $pub$
|
|
136
|
-
begin
|
|
137
|
-
if not exists (select 1 from pg_publication where pubname = 'supabase_realtime') then create publication supabase_realtime; end if;
|
|
138
|
-
end
|
|
139
|
-
$pub$;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Managed by starci app sync (slot app.db.image); do not edit: run "npx starci app sync --write".
|
|
2
|
+
# The image of the db service of a lite app, kept with the stacks (rulings lite-official-postgres-image, lite-pgmq-bus-bullmq-jobs and
|
|
3
|
+
# lite-db-image-in-stacks): the Docker Official postgres image, pinned by digest, plus pgvector from the PGDG repository the image
|
|
4
|
+
# already configures and pgmq, the event bus, from its pinned release source verified by sha256 (pgmq is SQL objects only, so its
|
|
5
|
+
# control and script files are copied and nothing is compiled; it installs into its own schema pgmq). The contrib extensions
|
|
6
|
+
# (pgcrypto, pg_trgm, unaccent, uuid-ossp, btree_gist) ship in the image. The stack's db service, `npm run db:create`,
|
|
7
|
+
# `starci app db drift` and `starci app db baseline --squash` all build this file, with this folder as the build context.
|
|
8
|
+
FROM {{dbBaseImage}}
|
|
9
|
+
ADD --checksum=sha256:{{pgmqSha256}} {{pgmqSource}} /tmp/pgmq.tar.gz
|
|
10
|
+
RUN apt-get update \
|
|
11
|
+
&& apt-get install -y --no-install-recommends {{pgvectorPackage}} \
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
13
|
+
&& tar -xzf /tmp/pgmq.tar.gz -C /tmp \
|
|
14
|
+
&& extension="$(pg_config --sharedir)/extension" \
|
|
15
|
+
&& cp /tmp/pgmq-{{pgmqVersion}}/pgmq-extension/pgmq.control "$extension/" \
|
|
16
|
+
&& cp /tmp/pgmq-{{pgmqVersion}}/pgmq-extension/sql/pgmq.sql "$extension/pgmq--{{pgmqVersion}}.sql" \
|
|
17
|
+
&& cp /tmp/pgmq-{{pgmqVersion}}/pgmq-extension/sql/pgmq--*--*.sql "$extension/" \
|
|
18
|
+
&& rm -rf /tmp/pgmq.tar.gz /tmp/pgmq-{{pgmqVersion}}
|
package/templates/app/gitignore
CHANGED
|
@@ -7,7 +7,7 @@ schema.gql
|
|
|
7
7
|
.next/
|
|
8
8
|
next-env.d.ts
|
|
9
9
|
# .starcistacks custody at the app root: modules/schemas/stacks-layout.yaml custody.gitignoreRules, in order. Only the
|
|
10
|
-
# declaration, runbooks, key lists, seeds, the rendered environment, the infra definitions and sealed *.enc are tracked.
|
|
10
|
+
# declaration, runbooks, key lists, seeds, the rendered environment, the infra definitions and sealed members (*.enc.yaml, legacy *.enc) are tracked.
|
|
11
11
|
.starcistacks/**
|
|
12
12
|
!.starcistacks/**/
|
|
13
13
|
!.starcistacks/**/KEYS.md
|
|
@@ -32,6 +32,7 @@ next-env.d.ts
|
|
|
32
32
|
.starcistacks/*/infra/terraform/*.tfstate.*
|
|
33
33
|
.starcistacks/*/infra/terraform/files/
|
|
34
34
|
.starcistacks/*/infra/terraform/*.tar.gz
|
|
35
|
+
!.starcistacks/**/secrets/*.enc.yaml
|
|
35
36
|
!.starcistacks/**/*.enc
|
|
36
37
|
|
|
37
38
|
# Host-local agent credentials and app age identity never enter Git.
|
|
@@ -7,12 +7,13 @@ schema.gql
|
|
|
7
7
|
.next/
|
|
8
8
|
next-env.d.ts
|
|
9
9
|
# .starcistacks custody at the app root: modules/schemas/stacks-layout.yaml custody.gitignoreRules, in order. Only the
|
|
10
|
-
# declaration, runbooks, key lists, seeds, the rendered environment, the infra definitions and sealed *.enc are tracked.
|
|
10
|
+
# declaration, runbooks, key lists, seeds, the rendered environment, the infra definitions, the db image and sealed members (*.enc.yaml, legacy *.enc) are tracked.
|
|
11
11
|
.starcistacks/**
|
|
12
12
|
!.starcistacks/**/
|
|
13
13
|
!.starcistacks/**/KEYS.md
|
|
14
14
|
!.starcistacks/**/.gitkeep
|
|
15
15
|
!.starcistacks/application-stacks.yaml
|
|
16
|
+
!.starcistacks/images/db/Dockerfile
|
|
16
17
|
!.starcistacks/*/README.md
|
|
17
18
|
!.starcistacks/*/environment.json
|
|
18
19
|
!.starcistacks/*/seeds/**
|
|
@@ -32,6 +33,7 @@ next-env.d.ts
|
|
|
32
33
|
.starcistacks/*/infra/terraform/*.tfstate.*
|
|
33
34
|
.starcistacks/*/infra/terraform/files/
|
|
34
35
|
.starcistacks/*/infra/terraform/*.tar.gz
|
|
36
|
+
!.starcistacks/**/secrets/*.enc.yaml
|
|
35
37
|
!.starcistacks/**/*.enc
|
|
36
38
|
|
|
37
39
|
# Host-local agent credentials and app age identity never enter Git.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"db:
|
|
1
|
+
"db:image": "docker build --tag {{dbImage}} .starcistacks/images/db",
|
|
2
|
+
"db:create": "npm run db:image && docker create --name {{project}}-dev-db -p 127.0.0.1:54322:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -v {{project}}-dev-db-data:/var/lib/postgresql/data {{dbImage}}",
|
|
2
3
|
"db:start": "docker start {{project}}-dev-db",
|
|
3
4
|
"db:stop": "docker stop {{project}}-dev-db",
|
|
4
5
|
"db:reset": "docker rm -f {{project}}-dev-db && docker volume rm {{project}}-dev-db-data && npm run db:create && npm run db:start",
|
|
@@ -27,9 +27,9 @@ components:
|
|
|
27
27
|
role: service
|
|
28
28
|
stateful: true
|
|
29
29
|
volumes: [db-data]
|
|
30
|
-
image: "
|
|
30
|
+
image: "{{project}}/db:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
31
31
|
compose: infra/swarm/stack.yaml
|
|
32
|
-
purpose: The app's own PostgreSQL
|
|
32
|
+
purpose: The app's own PostgreSQL, the official postgres image plus pgvector built from .starcistacks/images/db/Dockerfile, migrated by starci app db migrate.
|
|
33
33
|
required: true
|
|
34
34
|
ingress:
|
|
35
35
|
role: ingress
|
|
@@ -203,6 +203,8 @@ environments:
|
|
|
203
203
|
images:
|
|
204
204
|
api:
|
|
205
205
|
app: api
|
|
206
|
+
db:
|
|
207
|
+
app: db
|
|
206
208
|
web:
|
|
207
209
|
app: web
|
|
208
210
|
# Public browser configuration; replace these placeholders before deployment.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Development stack
|
|
2
2
|
|
|
3
|
-
Every development dependency runs in Docker Linux containers. The `db` service (
|
|
3
|
+
Every development dependency runs in Docker Linux containers. The `db` service (the official postgres image plus pgvector, built from `.starcistacks/images/db/Dockerfile`) starts with `npm run db:start` and takes the migrations with `npm run db:push`; storage is the S3-compatible MinIO the environment names. Every other declared dependency, such as Redis, starts through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model; Compose is an implementation detail. The app services may run in the local Docker Swarm model or through a declared native development profile while dependencies stay in Docker.
|
|
4
4
|
The application image references contain illustrative all-zero digests. Replace them with verified published release digests in the manifest, model and `.starcistacks/dev/infra/swarm/releases/images.lock.yaml` before starting the container model. Seal the local database connection and the integration credentials; a container cannot use the host's loopback address without owner-configured networking. Local source development remains available through the app scripts.
|
|
5
5
|
|
|
6
6
|
The runtime template tracks no encrypted members. Scaffolding generates the six declared members with placeholder values encrypted to this app's own recipient in `.sops.yaml`. Replace those values with `starci app secret set dev` using the matching host age identity in untracked local custody. Existing generated envelopes are decryptable with that app identity and can be updated in place. Each Docker secret contains the corresponding key's real value, and the API receives it through its declared mounted file.
|
|
@@ -83,7 +83,7 @@ services:
|
|
|
83
83
|
condition: on-failure
|
|
84
84
|
delay: 5s
|
|
85
85
|
db:
|
|
86
|
-
image: "
|
|
86
|
+
image: "{{project}}/db:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
87
87
|
environment:
|
|
88
88
|
POSTGRES_DB: postgres
|
|
89
89
|
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Production stack
|
|
2
2
|
|
|
3
3
|
The tracked model declares Docker Swarm application services and one digest-pinned Caddy ingress.
|
|
4
|
-
The `db` service (image
|
|
4
|
+
The `db` service (the official postgres image plus pgvector, built from `.starcistacks/images/db/Dockerfile` and pushed with the release images) supplies PostgreSQL on a named volume; sign-in is the API's own (Passport) and storage is the S3-compatible MinIO the environment names. The app and its dependencies run on Docker Swarm. A Redis service runs only when `hfs.json` `sides.be.backing` declares it for a capability that needs it: one stateful replica with a digest-pinned image, a named volume with a backup entry and its password as a sealed member granted as a Swarm secret (Redis runs `--appendonly yes` with the volume at `/data`). The scaffold declares none; `starci app add queue|job` declares Redis and renders it here.
|
|
5
5
|
The model is a deployment declaration. It contains no owner credentials, real DNS receipt, completed release or restore proof. Public records under `.starcistacks/production/infra/swarm/` explicitly mark those owner-run operations as pending.
|
|
6
6
|
Its application image references contain illustrative all-zero digests; they identify no published image and must be replaced before deployment.
|
|
7
7
|
|
|
@@ -83,7 +83,7 @@ services:
|
|
|
83
83
|
condition: on-failure
|
|
84
84
|
delay: 5s
|
|
85
85
|
db:
|
|
86
|
-
image: "
|
|
86
|
+
image: "{{project}}/db:v0.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
87
87
|
environment:
|
|
88
88
|
POSTGRES_DB: postgres
|
|
89
89
|
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
|
|
@@ -8,7 +8,7 @@ The starter keeps one deployable API and one web app, with the same code structu
|
|
|
8
8
|
|
|
9
9
|
## Stack
|
|
10
10
|
|
|
11
|
-
TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, PostgreSQL (the `db` service
|
|
11
|
+
TypeScript everywhere, NestJS in `be/`, Next.js with next-intl in `fe/`, PostgreSQL (the `db` service: the official postgres image plus pgvector, built from `.starcistacks/images/db/Dockerfile`), Passport for Google and password sign-in, S3-compatible storage (MinIO), npm workspaces, and Turbo for front-end tasks.
|
|
12
12
|
|
|
13
13
|
## Repository layout
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ Declare `ci.github.repository` and `ci.github.defaultBranch` in `.starcistacks/a
|
|
|
24
24
|
|
|
25
25
|
## Development
|
|
26
26
|
|
|
27
|
-
Every development dependency runs in Docker Linux containers. Create the local database once with `npm run db:create`, start it with `npm run db:start` (one container of the
|
|
27
|
+
Every development dependency runs in Docker Linux containers. Create the local database once with `npm run db:create`, start it with `npm run db:start` (one container of the image `.starcistacks/images/db/Dockerfile` builds, on 127.0.0.1:54322), export `DATABASE_URL=postgresql://postgres@127.0.0.1:54322/postgres` and apply `db/bootstrap.sql` and the migrations with `npm run db:push`; start any other declared dependency through its managed `<dep>:create`, `<dep>:start` and `<dep>:stop` scripts or the declared container model. Compose is an implementation detail. Configure the local environment and start the API and web app using the package scripts for the declared development profile. In production, the app and its dependencies, the database included, run on Docker Swarm; pgvector owns vector search.
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
30
|
npm install
|
|
@@ -3,6 +3,8 @@ import type { ParsedEvent } from "@modules/platform/event-bus"
|
|
|
3
3
|
|
|
4
4
|
/** A durable receipt reference; processing reads the provider payload after the intake commits. */
|
|
5
5
|
export interface @@Provider@@DeliveryRecordedPayload {
|
|
6
|
+
/** The business key a consumer's inbox claims: the receipt, once per consumer however often it is delivered. */
|
|
7
|
+
readonly dedupeKey: string
|
|
6
8
|
readonly receiptId: string
|
|
7
9
|
}
|
|
8
10
|
|
|
@@ -20,17 +22,17 @@ export class @@Provider@@DeliveryRecordedEvent extends BaseEvent {
|
|
|
20
22
|
super()
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/** The receipt identity is stable across
|
|
24
|
-
static create(payload:
|
|
25
|
-
return new @@Provider@@DeliveryRecordedEvent(payload.receiptId, payload)
|
|
25
|
+
/** The receipt identity is stable across redeliveries and distinct from a consumer's inbox source. */
|
|
26
|
+
static create(payload: Omit<@@Provider@@DeliveryRecordedPayload, "dedupeKey">): @@Provider@@DeliveryRecordedEvent {
|
|
27
|
+
return new @@Provider@@DeliveryRecordedEvent(payload.receiptId, { dedupeKey: payload.receiptId, receiptId: payload.receiptId })
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/** Reads
|
|
30
|
+
/** Reads the event envelope; malformed deliveries never reach a processing consumer. */
|
|
29
31
|
static parse(envelope: unknown): ParsedEvent<@@Provider@@DeliveryRecordedEvent> {
|
|
30
32
|
if (typeof envelope !== "object" || envelope === null || !("eventId" in envelope) || !("payload" in envelope)) return null
|
|
31
33
|
const { eventId, payload } = envelope
|
|
32
34
|
if (typeof eventId !== "string" || !eventId.trim() || typeof payload !== "object" || payload === null || !("receiptId" in payload)) return null
|
|
33
35
|
if (typeof payload.receiptId !== "string" || !payload.receiptId.trim()) return null
|
|
34
|
-
return new @@Provider@@DeliveryRecordedEvent(eventId, { receiptId: payload.receiptId })
|
|
36
|
+
return new @@Provider@@DeliveryRecordedEvent(eventId, { dedupeKey: payload.receiptId, receiptId: payload.receiptId })
|
|
35
37
|
}
|
|
36
38
|
}
|