@ankhorage/infra 7.0.0 → 7.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/commands/destroy.d.ts.map +1 -1
  3. package/dist/cli/commands/destroy.js +1 -0
  4. package/dist/cli/commands/destroy.js.map +1 -1
  5. package/dist/cli/utils/prepareInfraCommandOperationAsync.d.ts.map +1 -1
  6. package/dist/cli/utils/prepareInfraCommandOperationAsync.js +4 -1
  7. package/dist/cli/utils/prepareInfraCommandOperationAsync.js.map +1 -1
  8. package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.d.ts +11 -0
  9. package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.d.ts.map +1 -0
  10. package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.js +125 -0
  11. package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.js.map +1 -0
  12. package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.d.ts +4 -0
  13. package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.d.ts.map +1 -0
  14. package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js +9 -0
  15. package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js.map +1 -0
  16. package/dist/features/environment-lifecycle/composition/createInfraCommandServices.d.ts.map +1 -1
  17. package/dist/features/environment-lifecycle/composition/createInfraCommandServices.js +9 -3
  18. package/dist/features/environment-lifecycle/composition/createInfraCommandServices.js.map +1 -1
  19. package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.d.ts.map +1 -1
  20. package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.js +28 -2
  21. package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.js.map +1 -1
  22. package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.d.ts +4 -0
  23. package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.d.ts.map +1 -0
  24. package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.js +11 -0
  25. package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.js.map +1 -0
  26. package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.d.ts +1 -1
  27. package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.d.ts.map +1 -1
  28. package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.js +2 -6
  29. package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.js.map +1 -1
  30. package/dist/types/infraCli.d.ts +7 -1
  31. package/dist/types/infraCli.d.ts.map +1 -1
  32. package/dist/types/infraCli.js.map +1 -1
  33. package/package.json +3 -2
  34. package/src/cli/commands/destroy.ts +1 -0
  35. package/src/cli/utils/prepareInfraCommandOperationAsync.ts +4 -1
  36. package/src/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.ts +182 -0
  37. package/src/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.ts +18 -0
  38. package/src/features/environment-lifecycle/composition/createInfraCommandServices.ts +9 -3
  39. package/src/features/environment-lifecycle/composition/createProjectInfraLifecycle.ts +33 -4
  40. package/src/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.ts +16 -0
  41. package/src/features/environment-lifecycle/utils/resolveInfraStatePath.ts +3 -6
  42. package/src/projectCredentialLifecycle.test.ts +161 -0
  43. package/src/projectCredentialStore.test.ts +151 -0
  44. package/src/types/infraCli.ts +11 -1
@@ -1 +1 @@
1
- {"version":3,"file":"infraCli.d.ts","sourceRoot":"","sources":["../../src/types/infraCli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"infraCli.d.ts","sourceRoot":"","sources":["../../src/types/infraCli.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"infraCli.js","sourceRoot":"","sources":["../../src/types/infraCli.ts"],"names":[],"mappings":"","sourcesContent":["import type { AnkhCapabilityId } from '@ankhorage/contracts/cli';\nimport type { AppEnvironmentId } from '@ankhorage/contracts/environments';\nimport type { InfraEnvironmentSpec } from '@ankhorage/contracts/infra';\n\nimport type { readStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/readStoredInfraStateAsync.js';\nimport type { removeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraStateAsync.js';\nimport type { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';\nimport type { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';\nimport type { destroyInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/destroyInfraEnvironmentAsync.js';\nimport type { downInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/downInfraEnvironmentAsync.js';\nimport type { generateInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/generateInfraEnvironmentAsync.js';\nimport type { getInfraEnvironmentOutputs } from '../features/environment-lifecycle/application/use-cases/getInfraEnvironmentOutputs.js';\nimport type { planInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/planInfraEnvironmentAsync.js';\nimport type { statusInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/statusInfraEnvironmentAsync.js';\nimport type { upInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/upInfraEnvironmentAsync.js';\nimport type { validateInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/validateInfraEnvironmentAsync.js';\nimport type {\n InfraOperationRequest,\n InfraOrchestrationDependencies,\n InfraStoredState,\n} from './infraOrchestration.js';\nimport type { ResolvedInfraProject } from './infraProject.js';\n\ntype InfraCommandName =\n 'validate' | 'plan' | 'generate' | 'up' | 'status' | 'outputs' | 'down' | 'destroy';\n\nexport interface InfraCommandContext {\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n readonly version: string;\n writeStdout(text: string): void;\n writeStderr(text: string): void;\n}\n\nexport interface InfraCommandRunResult {\n readonly exitCode: number;\n}\n\nexport interface ParsedInfraArguments {\n readonly projectId?: string;\n readonly environment?: AppEnvironmentId;\n readonly format: 'human' | 'json' | 'env';\n readonly confirmation?: string;\n readonly deleteResources: readonly string[];\n}\n\nexport interface InfraCommandRunRequest {\n readonly context: InfraCommandContext;\n readonly arguments: ParsedInfraArguments;\n}\n\ntype InfraCommandImplementation = (\n request: InfraCommandRunRequest,\n services: InfraCommandServices,\n) => Promise<InfraCommandRunResult>;\n\nexport interface InfraCommandDefinition {\n readonly capability: AnkhCapabilityId;\n readonly path: readonly [InfraCommandName];\n readonly standaloneName: InfraCommandName;\n readonly summary: string;\n readonly run: InfraCommandImplementation;\n}\n\nexport interface InfraCommandInvocation {\n readonly argv: readonly string[];\n readonly command: InfraCommandDefinition;\n readonly context: InfraCommandContext;\n}\n\nexport interface InfraLifecycleOperations {\n readonly validate: typeof validateInfraEnvironmentAsync;\n readonly plan: typeof planInfraEnvironmentAsync;\n readonly generate: typeof generateInfraEnvironmentAsync;\n readonly up: typeof upInfraEnvironmentAsync;\n readonly status: typeof statusInfraEnvironmentAsync;\n readonly outputs: typeof getInfraEnvironmentOutputs;\n readonly down: typeof downInfraEnvironmentAsync;\n readonly destroy: typeof destroyInfraEnvironmentAsync;\n}\n\nexport interface InfraCommandServices {\n readonly resolveProject: (options: {\n readonly cwd: string;\n readonly projectId?: string;\n }) => Promise<ResolvedInfraProject>;\n readonly readState: typeof readStoredInfraStateAsync;\n readonly writeState: typeof writeStoredInfraStateAsync;\n readonly removeState: typeof removeStoredInfraStateAsync;\n readonly writeArtifacts: typeof writeInfraGeneratedArtifactsAsync;\n readonly operations: InfraLifecycleOperations;\n readonly createDependencies: (context: InfraCommandContext) => InfraOrchestrationDependencies;\n}\n\nexport interface RunInfraCommandOptions {\n readonly services?: Partial<InfraCommandServices>;\n}\n\nexport interface CreateInfraRuntimeProviderOptions {\n readonly runCommandImpl?: RunInfraCommandImpl;\n readonly services?: Partial<InfraCommandServices>;\n}\n\nexport type RunInfraCommandImpl = (\n request: InfraCommandInvocation,\n options?: RunInfraCommandOptions,\n) => Promise<InfraCommandRunResult>;\n\nexport interface PreparedInfraCommandOperation {\n readonly project: ResolvedInfraProject;\n readonly state: InfraStoredState | null;\n readonly desired: InfraEnvironmentSpec;\n readonly operation: InfraOperationRequest;\n readonly dependencies: InfraOrchestrationDependencies;\n}\n"]}
1
+ {"version":3,"file":"infraCli.js","sourceRoot":"","sources":["../../src/types/infraCli.ts"],"names":[],"mappings":"","sourcesContent":["import type { AnkhCapabilityId } from '@ankhorage/contracts/cli';\nimport type { AppEnvironmentId } from '@ankhorage/contracts/environments';\nimport type { InfraEnvironmentSpec } from '@ankhorage/contracts/infra';\n\nimport type { readStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/readStoredInfraStateAsync.js';\nimport type { removeStoredInfraCredentialsAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js';\nimport type { removeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraStateAsync.js';\nimport type { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';\nimport type { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';\nimport type { destroyInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/destroyInfraEnvironmentAsync.js';\nimport type { downInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/downInfraEnvironmentAsync.js';\nimport type { generateInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/generateInfraEnvironmentAsync.js';\nimport type { getInfraEnvironmentOutputs } from '../features/environment-lifecycle/application/use-cases/getInfraEnvironmentOutputs.js';\nimport type { planInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/planInfraEnvironmentAsync.js';\nimport type { statusInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/statusInfraEnvironmentAsync.js';\nimport type { upInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/upInfraEnvironmentAsync.js';\nimport type { validateInfraEnvironmentAsync } from '../features/environment-lifecycle/application/use-cases/validateInfraEnvironmentAsync.js';\nimport type {\n InfraOperationRequest,\n InfraOrchestrationDependencies,\n InfraStoredState,\n} from './infraOrchestration.js';\nimport type { ResolvedInfraProject } from './infraProject.js';\n\ntype InfraCommandName =\n 'validate' | 'plan' | 'generate' | 'up' | 'status' | 'outputs' | 'down' | 'destroy';\n\nexport interface InfraCommandContext {\n readonly cwd: string;\n readonly env: Readonly<Record<string, string | undefined>>;\n readonly version: string;\n writeStdout(text: string): void;\n writeStderr(text: string): void;\n}\n\nexport interface InfraCommandRunResult {\n readonly exitCode: number;\n}\n\nexport interface ParsedInfraArguments {\n readonly projectId?: string;\n readonly environment?: AppEnvironmentId;\n readonly format: 'human' | 'json' | 'env';\n readonly confirmation?: string;\n readonly deleteResources: readonly string[];\n}\n\nexport interface InfraCommandRunRequest {\n readonly context: InfraCommandContext;\n readonly arguments: ParsedInfraArguments;\n}\n\ntype InfraCommandImplementation = (\n request: InfraCommandRunRequest,\n services: InfraCommandServices,\n) => Promise<InfraCommandRunResult>;\n\nexport interface InfraCommandDefinition {\n readonly capability: AnkhCapabilityId;\n readonly path: readonly [InfraCommandName];\n readonly standaloneName: InfraCommandName;\n readonly summary: string;\n readonly run: InfraCommandImplementation;\n}\n\nexport interface InfraCommandInvocation {\n readonly argv: readonly string[];\n readonly command: InfraCommandDefinition;\n readonly context: InfraCommandContext;\n}\n\nexport interface InfraLifecycleOperations {\n readonly validate: typeof validateInfraEnvironmentAsync;\n readonly plan: typeof planInfraEnvironmentAsync;\n readonly generate: typeof generateInfraEnvironmentAsync;\n readonly up: typeof upInfraEnvironmentAsync;\n readonly status: typeof statusInfraEnvironmentAsync;\n readonly outputs: typeof getInfraEnvironmentOutputs;\n readonly down: typeof downInfraEnvironmentAsync;\n readonly destroy: typeof destroyInfraEnvironmentAsync;\n}\n\ninterface InfraDependencyScope {\n readonly projectPath: string;\n readonly environment: AppEnvironmentId;\n}\n\nexport interface InfraCommandServices {\n readonly resolveProject: (options: {\n readonly cwd: string;\n readonly projectId?: string;\n }) => Promise<ResolvedInfraProject>;\n readonly readState: typeof readStoredInfraStateAsync;\n readonly writeState: typeof writeStoredInfraStateAsync;\n readonly removeState: typeof removeStoredInfraStateAsync;\n readonly removeCredentials: typeof removeStoredInfraCredentialsAsync;\n readonly writeArtifacts: typeof writeInfraGeneratedArtifactsAsync;\n readonly operations: InfraLifecycleOperations;\n readonly createDependencies: (\n context: InfraCommandContext,\n scope: InfraDependencyScope,\n ) => InfraOrchestrationDependencies;\n}\n\nexport interface RunInfraCommandOptions {\n readonly services?: Partial<InfraCommandServices>;\n}\n\nexport interface CreateInfraRuntimeProviderOptions {\n readonly runCommandImpl?: RunInfraCommandImpl;\n readonly services?: Partial<InfraCommandServices>;\n}\n\nexport type RunInfraCommandImpl = (\n request: InfraCommandInvocation,\n options?: RunInfraCommandOptions,\n) => Promise<InfraCommandRunResult>;\n\nexport interface PreparedInfraCommandOperation {\n readonly project: ResolvedInfraProject;\n readonly state: InfraStoredState | null;\n readonly desired: InfraEnvironmentSpec;\n readonly operation: InfraOperationRequest;\n readonly dependencies: InfraOrchestrationDependencies;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "7.0.0",
4
+ "version": "7.1.0",
5
5
  "description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
6
6
  "homepage": "https://github.com/ankhorage/infra#readme",
7
7
  "bugs": {
@@ -35,7 +35,8 @@
35
35
  "openapi"
36
36
  ],
37
37
  "dependencies": {
38
- "@ankhorage/contracts": "^21.0.0"
38
+ "@ankhorage/contracts": "^21.0.0",
39
+ "@ankhorage/utility": "^1.2.0"
39
40
  },
40
41
  "files": [
41
42
  "dist",
@@ -35,6 +35,7 @@ export const destroy = {
35
35
  await services.writeArtifacts(operation.project.projectPath, [], operation.state?.ledger);
36
36
  if (result.value.ledger === null) {
37
37
  await services.removeState(operation.project.projectPath, environment);
38
+ await services.removeCredentials(operation.project.projectPath, environment);
38
39
  } else {
39
40
  await services.writeState(operation.project.projectPath, {
40
41
  schemaVersion: 1,
@@ -37,7 +37,10 @@ export async function prepareInfraCommandOperationAsync(
37
37
  ...(request.arguments.environment === undefined ? {} : { environment }),
38
38
  ...(state === null ? {} : { previous: state.ledger, previousDesired: state.desired }),
39
39
  },
40
- dependencies: services.createDependencies(request.context),
40
+ dependencies: services.createDependencies(request.context, {
41
+ projectPath: project.projectPath,
42
+ environment,
43
+ }),
41
44
  };
42
45
  }
43
46
 
@@ -0,0 +1,182 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
6
+ import type {
7
+ InfraControlPlaneCredentialRef,
8
+ InfraCredentialPort,
9
+ InfraResult,
10
+ } from '@ankhorage/contracts/infra';
11
+ import { isRecord, isRecordOf } from '@ankhorage/utility/object';
12
+
13
+ import { createInfraFailure } from '../../utils/createInfraFailure.js';
14
+ import { normalizeInfraEnvironmentKey } from '../../utils/normalizeInfraEnvironmentKey.js';
15
+ import { resolveInfraEnvironmentStateDirectory } from '../../utils/resolveInfraEnvironmentStateDirectory.js';
16
+ import { createEnvironmentInfraCredentialPort } from './createEnvironmentInfraCredentialPort.js';
17
+
18
+ /*** Resolve explicit environment overrides before durable project-local bootstrap credentials. */
19
+ export function createProjectInfraCredentialPort(
20
+ options: ProjectInfraCredentialPortOptions,
21
+ ): InfraCredentialPort {
22
+ const environmentPort = createEnvironmentInfraCredentialPort(options.processEnvironment);
23
+ return {
24
+ findAsync: (reference) => findProjectCredentialAsync(options, environmentPort, reference),
25
+ async resolveAsync(reference) {
26
+ const found = await findProjectCredentialAsync(options, environmentPort, reference);
27
+ if (!found.ok) return found;
28
+ if (found.value !== null) {
29
+ return { ok: true, value: found.value, diagnostics: found.diagnostics };
30
+ }
31
+ return missingCredential(reference.name);
32
+ },
33
+ async persistAsync(reference, values) {
34
+ if (!isStringRecord(values)) {
35
+ return createInfraFailure(
36
+ 'infra-control-plane-credential-invalid',
37
+ `Control-plane credential ${reference.name} must contain only string fields.`,
38
+ );
39
+ }
40
+ return writeStoredCredentialAsync(options, reference, values);
41
+ },
42
+ };
43
+ }
44
+
45
+ interface ProjectInfraCredentialPortOptions {
46
+ readonly projectPath: string;
47
+ readonly environment: AppEnvironmentId;
48
+ readonly processEnvironment: Readonly<Record<string, string | undefined>>;
49
+ }
50
+
51
+ interface StoredCredentialBundle {
52
+ readonly schemaVersion: 1;
53
+ readonly name: string;
54
+ readonly values: Readonly<Record<string, string>>;
55
+ }
56
+
57
+ /*** Resolve environment precedence before falling through to project-local state. */
58
+ async function findProjectCredentialAsync(
59
+ options: ProjectInfraCredentialPortOptions,
60
+ environmentPort: InfraCredentialPort,
61
+ reference: InfraControlPlaneCredentialRef,
62
+ ): Promise<InfraResult<Readonly<Record<string, string>> | null>> {
63
+ const overridden = await environmentPort.findAsync(reference);
64
+ if (!overridden.ok || overridden.value !== null) return overridden;
65
+ return readStoredCredentialAsync(options, reference);
66
+ }
67
+
68
+ /*** Read one exact credential bundle without enumerating or exposing sibling credentials. */
69
+ async function readStoredCredentialAsync(
70
+ options: ProjectInfraCredentialPortOptions,
71
+ reference: InfraControlPlaneCredentialRef,
72
+ ): Promise<InfraResult<Readonly<Record<string, string>> | null>> {
73
+ const credentialPath = resolveCredentialPath(options, reference.name);
74
+ try {
75
+ const parsed: unknown = JSON.parse(await fs.readFile(credentialPath, 'utf8'));
76
+ if (!isStoredCredentialBundle(parsed) || parsed.name !== reference.name) {
77
+ return invalidCredentialStore(reference.name);
78
+ }
79
+ return { ok: true, value: parsed.values, diagnostics: [] };
80
+ } catch (error) {
81
+ if (isMissingFile(error)) return { ok: true, value: null, diagnostics: [] };
82
+ if (error instanceof SyntaxError) return invalidCredentialStore(reference.name);
83
+ return createInfraFailure(
84
+ 'infra-control-plane-credential-store-read-failed',
85
+ `Control-plane credential ${reference.name} could not be read from project state.`,
86
+ );
87
+ }
88
+ }
89
+
90
+ /*** Persist one opaque bundle atomically with restrictive local-host permissions. */
91
+ async function writeStoredCredentialAsync(
92
+ options: ProjectInfraCredentialPortOptions,
93
+ reference: InfraControlPlaneCredentialRef,
94
+ values: Readonly<Record<string, string>>,
95
+ ): Promise<InfraResult<null>> {
96
+ const credentialPath = resolveCredentialPath(options, reference.name);
97
+ const directory = path.dirname(credentialPath);
98
+ const temporaryPath = `${credentialPath}.${process.pid}.${randomUUID()}.tmp`;
99
+ const stored: StoredCredentialBundle = { schemaVersion: 1, name: reference.name, values };
100
+ try {
101
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
102
+ await restrictPermissionsAsync(directory, 0o700);
103
+ await fs.writeFile(temporaryPath, `${JSON.stringify(stored, null, 2)}\n`, {
104
+ encoding: 'utf8',
105
+ mode: 0o600,
106
+ flag: 'wx',
107
+ });
108
+ await fs.rename(temporaryPath, credentialPath);
109
+ await restrictPermissionsAsync(credentialPath, 0o600);
110
+ return { ok: true, value: null, diagnostics: [] };
111
+ } catch {
112
+ await removeTemporaryCredentialAsync(temporaryPath);
113
+ return createInfraFailure(
114
+ 'infra-control-plane-credential-store-write-failed',
115
+ `Control-plane credential ${reference.name} could not be persisted to project state.`,
116
+ );
117
+ }
118
+ }
119
+
120
+ /*** Remove an abandoned unique temporary credential file without masking the original write failure. */
121
+ async function removeTemporaryCredentialAsync(temporaryPath: string): Promise<void> {
122
+ try {
123
+ await fs.rm(temporaryPath, { force: true });
124
+ } catch {
125
+ return;
126
+ }
127
+ }
128
+
129
+ /*** Resolve one credential to an opaque hash-named file inside its project/environment scope. */
130
+ function resolveCredentialPath(options: ProjectInfraCredentialPortOptions, name: string): string {
131
+ const digest = createHash('sha256').update(name).digest('hex');
132
+ return path.join(
133
+ resolveInfraEnvironmentStateDirectory(options.projectPath, options.environment),
134
+ 'credentials',
135
+ `${digest}.json`,
136
+ );
137
+ }
138
+
139
+ /*** Apply owner-only permissions where the host platform supports POSIX file modes. */
140
+ async function restrictPermissionsAsync(targetPath: string, mode: number): Promise<void> {
141
+ if (process.platform !== 'win32') await fs.chmod(targetPath, mode);
142
+ }
143
+
144
+ /*** Create the canonical missing-credential diagnostic after every trusted source was checked. */
145
+ function missingCredential(name: string): InfraResult<never> {
146
+ const prefixedName = `ANKH_INFRA_CREDENTIAL_${normalizeInfraEnvironmentKey(name)}`;
147
+ return createInfraFailure(
148
+ 'infra-control-plane-credential-missing',
149
+ `Control-plane credential ${name} is unavailable. Set ${name} or ${prefixedName}.`,
150
+ );
151
+ }
152
+
153
+ /*** Reject malformed persisted state without exposing or replacing any stored value. */
154
+ function invalidCredentialStore(name: string): InfraResult<never> {
155
+ return createInfraFailure(
156
+ 'infra-control-plane-credential-store-invalid',
157
+ `Stored control-plane credential ${name} has an invalid project-state shape.`,
158
+ );
159
+ }
160
+
161
+ /*** Recognize the private stored credential record. */
162
+ function isStoredCredentialBundle(value: unknown): value is StoredCredentialBundle {
163
+ if (!isRecord(value)) return false;
164
+ return (
165
+ value.schemaVersion === 1 && typeof value.name === 'string' && isStringRecord(value.values)
166
+ );
167
+ }
168
+
169
+ /*** Recognize string-only opaque provider credential bundles. */
170
+ function isStringRecord(value: unknown): value is Readonly<Record<string, string>> {
171
+ return isRecordOf(value, isString);
172
+ }
173
+
174
+ /*** Narrow one unknown value to a string. */
175
+ function isString(value: unknown): value is string {
176
+ return typeof value === 'string';
177
+ }
178
+
179
+ /*** Recognize only the filesystem's absent-file failure. */
180
+ function isMissingFile(error: unknown): boolean {
181
+ return isRecord(error) && error.code === 'ENOENT';
182
+ }
@@ -0,0 +1,18 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
5
+
6
+ import { resolveInfraEnvironmentStateDirectory } from '../../utils/resolveInfraEnvironmentStateDirectory.js';
7
+
8
+ /*** Remove only persisted generated credential state for one explicit project environment. */
9
+ export async function removeStoredInfraCredentialsAsync(
10
+ projectPath: string,
11
+ environment: AppEnvironmentId,
12
+ ): Promise<void> {
13
+ const credentialDirectory = path.join(
14
+ resolveInfraEnvironmentStateDirectory(projectPath, environment),
15
+ 'credentials',
16
+ );
17
+ await fs.rm(credentialDirectory, { recursive: true, force: true });
18
+ }
@@ -1,8 +1,9 @@
1
1
  import type { InfraCommandServices } from '../../../types/infraCli.js';
2
- import { createEnvironmentInfraCredentialPort } from '../adapters/outbound/createEnvironmentInfraCredentialPort.js';
3
2
  import { createEnvironmentInfraSecretPort } from '../adapters/outbound/createEnvironmentInfraSecretPort.js';
4
3
  import { createNodeInfraAdapterPackageResolver } from '../adapters/outbound/createNodeInfraAdapterPackageResolver.js';
4
+ import { createProjectInfraCredentialPort } from '../adapters/outbound/createProjectInfraCredentialPort.js';
5
5
  import { readStoredInfraStateAsync } from '../adapters/outbound/readStoredInfraStateAsync.js';
6
+ import { removeStoredInfraCredentialsAsync } from '../adapters/outbound/removeStoredInfraCredentialsAsync.js';
6
7
  import { removeStoredInfraStateAsync } from '../adapters/outbound/removeStoredInfraStateAsync.js';
7
8
  import { resolveInfraProjectAsync } from '../adapters/outbound/resolveInfraProjectAsync.js';
8
9
  import { writeInfraGeneratedArtifactsAsync } from '../adapters/outbound/writeInfraGeneratedArtifactsAsync.js';
@@ -25,6 +26,7 @@ export function createInfraCommandServices(
25
26
  readState: overrides.readState ?? readStoredInfraStateAsync,
26
27
  writeState: overrides.writeState ?? writeStoredInfraStateAsync,
27
28
  removeState: overrides.removeState ?? removeStoredInfraStateAsync,
29
+ removeCredentials: overrides.removeCredentials ?? removeStoredInfraCredentialsAsync,
28
30
  writeArtifacts: overrides.writeArtifacts ?? writeInfraGeneratedArtifactsAsync,
29
31
  operations: overrides.operations ?? {
30
32
  validate: validateInfraEnvironmentAsync,
@@ -38,9 +40,13 @@ export function createInfraCommandServices(
38
40
  },
39
41
  createDependencies:
40
42
  overrides.createDependencies ??
41
- ((context) => ({
43
+ ((context, scope) => ({
42
44
  adapterResolver: createNodeInfraAdapterPackageResolver(),
43
- credentials: createEnvironmentInfraCredentialPort(context.env),
45
+ credentials: createProjectInfraCredentialPort({
46
+ projectPath: scope.projectPath,
47
+ environment: scope.environment,
48
+ processEnvironment: context.env,
49
+ }),
44
50
  secrets: createEnvironmentInfraSecretPort(context.env),
45
51
  })),
46
52
  };
@@ -1,9 +1,10 @@
1
- import type { InfraEnvironmentSpec, InfraManifest } from '@ankhorage/contracts/infra';
1
+ import type { InfraEnvironmentSpec, InfraManifest, InfraResult } from '@ankhorage/contracts/infra';
2
2
 
3
3
  import { INFRA_PACKAGE_VERSION } from '../../../constants.js';
4
4
  import type { InfraOperationRequest, InfraStoredState } from '../../../types/infraOrchestration.js';
5
5
  import type {
6
6
  CreateProjectInfraLifecycleOptions,
7
+ ProjectInfraDestroyRequest,
7
8
  ProjectInfraLifecycle,
8
9
  ProjectInfraOperationRequest,
9
10
  } from '../../../types/infraProject.js';
@@ -72,6 +73,9 @@ export function createProjectInfraLifecycle(
72
73
  async destroyAsync(request) {
73
74
  const state = await services.readState(request.projectPath, request.environment);
74
75
  if (state === null) {
76
+ const safety = validateCredentialStateDestroy(request);
77
+ if (!safety.ok) return safety;
78
+ await services.removeCredentials(request.projectPath, request.environment);
75
79
  return {
76
80
  ok: true,
77
81
  value: { environment: request.environment, ledger: null },
@@ -89,9 +93,10 @@ export function createProjectInfraLifecycle(
89
93
  );
90
94
  if (!result.ok) return result;
91
95
  await services.writeArtifacts(request.projectPath, [], prepared.state?.ledger);
92
- if (result.value.ledger === null)
96
+ if (result.value.ledger === null) {
93
97
  await services.removeState(request.projectPath, request.environment);
94
- else {
98
+ await services.removeCredentials(request.projectPath, request.environment);
99
+ } else {
95
100
  await services.writeState(request.projectPath, {
96
101
  schemaVersion: 1,
97
102
  desired: prepared.desired,
@@ -142,10 +147,34 @@ async function prepareProjectOperationAsync(
142
147
  ...(state === null ? {} : { previous: state.ledger, previousDesired: state.desired }),
143
148
  ...(request.signal === undefined ? {} : { signal: request.signal }),
144
149
  },
145
- dependencies: services.createDependencies(context),
150
+ dependencies: services.createDependencies(context, {
151
+ projectPath: request.projectPath,
152
+ environment: request.environment,
153
+ }),
146
154
  };
147
155
  }
148
156
 
157
+ /*** Require exact scope confirmation before deleting orphaned project credential state. */
158
+ function validateCredentialStateDestroy(request: ProjectInfraDestroyRequest): InfraResult<null> {
159
+ if (
160
+ request.confirmation.projectId !== request.projectId ||
161
+ request.confirmation.environment !== request.environment
162
+ ) {
163
+ return {
164
+ ok: false,
165
+ diagnostics: [
166
+ {
167
+ severity: 'error',
168
+ code: 'infra-destroy-confirmation-mismatch',
169
+ message:
170
+ 'Infra destroy confirmation must exactly match the selected project and environment.',
171
+ },
172
+ ],
173
+ };
174
+ }
175
+ return { ok: true, value: null, diagnostics: [] };
176
+ }
177
+
149
178
  /*** Read one exact environment without inventing a fallback. */
150
179
  function readEnvironment(
151
180
  manifest: InfraManifest,
@@ -0,0 +1,16 @@
1
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
2
+ import { APP_ENVIRONMENT_IDS } from '@ankhorage/contracts/environments';
3
+ import path from 'path';
4
+
5
+ /*** Build the canonical project-local state directory for one closed Infra environment. */
6
+ export function resolveInfraEnvironmentStateDirectory(
7
+ projectPath: string,
8
+ environment: AppEnvironmentId,
9
+ ): string {
10
+ if (!APP_ENVIRONMENT_IDS.includes(environment)) {
11
+ throw new Error(`Invalid Infra state environment: ${environment}`);
12
+ }
13
+ return path.join(projectPath, INFRA_STATE_ROOT, environment);
14
+ }
15
+
16
+ const INFRA_STATE_ROOT = '.ankh/infra';
@@ -1,12 +1,9 @@
1
- import { APP_ENVIRONMENT_IDS, type AppEnvironmentId } from '@ankhorage/contracts/environments';
1
+ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
2
2
  import path from 'path';
3
3
 
4
- const INFRA_STATE_ROOT = '.ankh/infra';
4
+ import { resolveInfraEnvironmentStateDirectory } from './resolveInfraEnvironmentStateDirectory.js';
5
5
 
6
6
  /*** Build an exact state-file path from a closed canonical environment identifier. */
7
7
  export function resolveInfraStatePath(projectPath: string, environment: AppEnvironmentId): string {
8
- if (!APP_ENVIRONMENT_IDS.includes(environment)) {
9
- throw new Error(`Invalid Infra state environment: ${environment}`);
10
- }
11
- return path.join(projectPath, INFRA_STATE_ROOT, environment, 'state.json');
8
+ return path.join(resolveInfraEnvironmentStateDirectory(projectPath, environment), 'state.json');
12
9
  }
@@ -0,0 +1,161 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import type {
6
+ InfraControlPlaneCredentialRef,
7
+ InfraEnvironmentSpec,
8
+ InfraLedger,
9
+ InfraResult,
10
+ } from '@ankhorage/contracts/infra';
11
+ import { expect, it, mock } from 'bun:test';
12
+
13
+ import { createProjectInfraCredentialPort } from './features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.js';
14
+ import { createProjectInfraLifecycle } from './project/index.js';
15
+ import type { InfraLifecycleOperations } from './types/infraCli.js';
16
+
17
+ const bootstrap = {
18
+ source: 'control-plane',
19
+ name: 'FIXTURE_BOOTSTRAP',
20
+ } as const satisfies InfraControlPlaneCredentialRef;
21
+ const generated = { username: 'fixture', password: 'generated-secret' } as const;
22
+ const previewGenerated = { username: 'preview', password: 'preview-secret' } as const;
23
+
24
+ const desired = {
25
+ deployment: {
26
+ compute: { provider: 'local' },
27
+ runtime: { provider: 'docker-compose' },
28
+ },
29
+ } as const satisfies InfraEnvironmentSpec;
30
+
31
+ const ledger = {
32
+ schemaVersion: 1,
33
+ projectId: 'sample',
34
+ environment: 'local',
35
+ targets: [],
36
+ resources: [],
37
+ outputs: [],
38
+ artifacts: [],
39
+ } as const satisfies InfraLedger;
40
+
41
+ it('persists provider-created credentials across up/down and removes only the destroyed environment', async () => {
42
+ await withTempProjectAsync(async (projectPath) => {
43
+ const generateCredential = mock(() => generated);
44
+ const operations = createCredentialOperations(generateCredential);
45
+ const request = {
46
+ projectId: 'sample',
47
+ projectPath,
48
+ manifest: { environments: { local: desired, preview: desired }, modules: [] },
49
+ environment: 'local' as const,
50
+ executionEnvironment: {},
51
+ };
52
+
53
+ const firstLifecycle = createProjectInfraLifecycle({ services: { operations } });
54
+ const firstUp = await firstLifecycle.upAsync(request);
55
+ expect(firstUp.ok).toBe(true);
56
+ expect(generateCredential).toHaveBeenCalledTimes(1);
57
+ expect(JSON.stringify(firstUp)).not.toContain('generated-secret');
58
+ expect(await fs.readFile(statePath(projectPath, 'local'), 'utf8')).not.toContain(
59
+ 'generated-secret',
60
+ );
61
+
62
+ const down = await firstLifecycle.downAsync(request);
63
+ expect(down.ok).toBe(true);
64
+ expect(await createPort(projectPath, 'local').resolveAsync(bootstrap)).toMatchObject({
65
+ ok: true,
66
+ value: generated,
67
+ });
68
+
69
+ const secondLifecycle = createProjectInfraLifecycle({ services: { operations } });
70
+ const secondUp = await secondLifecycle.upAsync(request);
71
+ expect(secondUp.ok).toBe(true);
72
+ expect(generateCredential).toHaveBeenCalledTimes(1);
73
+ expect(JSON.stringify(secondUp)).not.toContain('generated-secret');
74
+
75
+ const previewPort = createPort(projectPath, 'preview');
76
+ expect(await previewPort.persistAsync(bootstrap, previewGenerated)).toMatchObject({ ok: true });
77
+
78
+ const destroyed = await secondLifecycle.destroyAsync({
79
+ ...request,
80
+ confirmation: { projectId: 'sample', environment: 'local' },
81
+ persistence: { policy: 'retain' },
82
+ });
83
+ expect(destroyed.ok).toBe(true);
84
+ expect(await createPort(projectPath, 'local').findAsync(bootstrap)).toEqual({
85
+ ok: true,
86
+ value: null,
87
+ diagnostics: [],
88
+ });
89
+ expect(await previewPort.resolveAsync(bootstrap)).toMatchObject({
90
+ ok: true,
91
+ value: previewGenerated,
92
+ });
93
+ });
94
+ });
95
+
96
+ /*** Create provider-neutral operation fixtures that generate credentials only when lookup is missing. */
97
+ function createCredentialOperations(
98
+ generateCredential: () => Readonly<Record<string, string>>,
99
+ ): InfraLifecycleOperations {
100
+ return {
101
+ validate: () => Promise.resolve(success({ environment: 'local' })),
102
+ plan: (request) =>
103
+ Promise.resolve(success({ projectId: request.projectId, environment: 'local', actions: [] })),
104
+ generate: () => Promise.resolve(success({ environment: 'local', artifacts: [], ledger })),
105
+ up: async (_request, dependencies) => {
106
+ const found = await dependencies.credentials.findAsync(bootstrap);
107
+ if (!found.ok) return { ok: false, diagnostics: found.diagnostics };
108
+ if (found.value === null) {
109
+ const persisted = await dependencies.credentials.persistAsync(
110
+ bootstrap,
111
+ generateCredential(),
112
+ );
113
+ if (!persisted.ok) return { ok: false, diagnostics: persisted.diagnostics };
114
+ }
115
+ return success({
116
+ environment: 'local',
117
+ targets: [],
118
+ resources: [],
119
+ outputs: [],
120
+ ledger,
121
+ });
122
+ },
123
+ status: (request) =>
124
+ Promise.resolve(
125
+ success({
126
+ projectId: request.projectId,
127
+ environment: 'local',
128
+ state: 'ready',
129
+ resources: [],
130
+ }),
131
+ ),
132
+ outputs: () => success({ environment: 'local', outputs: [] }),
133
+ down: () => Promise.resolve(success({ environment: 'local', ledger })),
134
+ destroy: () => Promise.resolve(success({ environment: 'local', ledger: null })),
135
+ };
136
+ }
137
+
138
+ /*** Create the real project-local credential adapter for one isolated test environment. */
139
+ function createPort(projectPath: string, environment: 'local' | 'preview') {
140
+ return createProjectInfraCredentialPort({ projectPath, environment, processEnvironment: {} });
141
+ }
142
+
143
+ /*** Resolve the canonical Infra ownership-state path without touching credential storage internals. */
144
+ function statePath(projectPath: string, environment: 'local' | 'preview'): string {
145
+ return path.join(projectPath, '.ankh', 'infra', environment, 'state.json');
146
+ }
147
+
148
+ /*** Create one successful Infra result for operation fixtures. */
149
+ function success<T>(value: T): InfraResult<T> {
150
+ return { ok: true, value, diagnostics: [] };
151
+ }
152
+
153
+ /*** Run one filesystem test against a disposable project root. */
154
+ async function withTempProjectAsync(run: (projectPath: string) => Promise<void>): Promise<void> {
155
+ const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ankh-infra-credential-lifecycle-'));
156
+ try {
157
+ await run(projectPath);
158
+ } finally {
159
+ await fs.rm(projectPath, { recursive: true, force: true });
160
+ }
161
+ }