@ankhorage/infra 7.0.0 → 7.1.1

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 (56) hide show
  1. package/CHANGELOG.md +12 -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/project/index.d.ts +1 -0
  31. package/dist/project/index.d.ts.map +1 -1
  32. package/dist/project/index.js +1 -0
  33. package/dist/project/index.js.map +1 -1
  34. package/dist/types/infraCli.d.ts +7 -1
  35. package/dist/types/infraCli.d.ts.map +1 -1
  36. package/dist/types/infraCli.js.map +1 -1
  37. package/package.json +4 -3
  38. package/src/cli/commands/destroy.ts +1 -0
  39. package/src/cli/utils/prepareInfraCommandOperationAsync.ts +4 -1
  40. package/src/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.ts +182 -0
  41. package/src/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.ts +18 -0
  42. package/src/features/environment-lifecycle/composition/createInfraCommandServices.ts +9 -3
  43. package/src/features/environment-lifecycle/composition/createProjectInfraLifecycle.ts +33 -4
  44. package/src/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.ts +16 -0
  45. package/src/features/environment-lifecycle/utils/resolveInfraStatePath.ts +3 -6
  46. package/src/hetznerK3sProduction.test.ts +29 -3
  47. package/src/hetznerK3sSupabaseR2.test.ts +33 -4
  48. package/src/localDockerComposeProviders.integration.test.ts +90 -51
  49. package/src/localK3sSupabase.test.ts +85 -27
  50. package/src/localMinikubeSupabase.integration.test.ts +175 -122
  51. package/src/localMinikubeSupabaseRecovery.integration.test.ts +37 -10
  52. package/src/project/index.test.ts +3 -1
  53. package/src/project/index.ts +1 -0
  54. package/src/projectCredentialLifecycle.test.ts +161 -0
  55. package/src/projectCredentialStore.test.ts +151 -0
  56. package/src/types/infraCli.ts +11 -1
@@ -0,0 +1,151 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import type { InfraControlPlaneCredentialRef } from '@ankhorage/contracts/infra';
7
+ import { expect, it } from 'bun:test';
8
+
9
+ import { createProjectInfraCredentialPort } from './features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort';
10
+
11
+ const bootstrap = reference('FIXTURE_BOOTSTRAP');
12
+ const secondary = reference('SECONDARY_BOOTSTRAP');
13
+ const generated = { username: 'fixture', password: 'generated-secret' } as const;
14
+
15
+ it('persists one opaque bundle and resolves it from a fresh host instance', async () => {
16
+ await withTempProjectAsync(async (projectPath) => {
17
+ const first = createPort(projectPath);
18
+ expect(await first.findAsync(bootstrap)).toEqual({ ok: true, value: null, diagnostics: [] });
19
+ expect(await first.persistAsync(bootstrap, generated)).toEqual({
20
+ ok: true,
21
+ value: null,
22
+ diagnostics: [],
23
+ });
24
+
25
+ const fresh = createPort(projectPath);
26
+ expect(await fresh.resolveAsync(bootstrap)).toEqual({
27
+ ok: true,
28
+ value: generated,
29
+ diagnostics: [],
30
+ });
31
+ });
32
+ });
33
+
34
+ it('keeps exact and prefixed environment overrides ahead of persisted state', async () => {
35
+ await withTempProjectAsync(async (projectPath) => {
36
+ const stored = createPort(projectPath);
37
+ await stored.persistAsync(bootstrap, generated);
38
+
39
+ const exact = createPort(projectPath, {
40
+ FIXTURE_BOOTSTRAP: JSON.stringify({ username: 'exact', password: 'override' }),
41
+ ANKH_INFRA_CREDENTIAL_FIXTURE_BOOTSTRAP: JSON.stringify({
42
+ username: 'prefixed',
43
+ password: 'override',
44
+ }),
45
+ });
46
+ expect(await exact.resolveAsync(bootstrap)).toMatchObject({
47
+ ok: true,
48
+ value: { username: 'exact', password: 'override' },
49
+ });
50
+
51
+ const prefixed = createPort(projectPath, {
52
+ ANKH_INFRA_CREDENTIAL_FIXTURE_BOOTSTRAP: JSON.stringify({
53
+ username: 'prefixed',
54
+ password: 'override',
55
+ }),
56
+ });
57
+ expect(await prefixed.resolveAsync(bootstrap)).toMatchObject({
58
+ ok: true,
59
+ value: { username: 'prefixed', password: 'override' },
60
+ });
61
+ });
62
+ });
63
+
64
+ it('fails closed on malformed persisted state instead of treating it as missing', async () => {
65
+ await withTempProjectAsync(async (projectPath) => {
66
+ const port = createPort(projectPath);
67
+ await port.persistAsync(bootstrap, generated);
68
+ await fs.writeFile(credentialPath(projectPath, 'local', bootstrap.name), '{broken', 'utf8');
69
+
70
+ const result = await createPort(projectPath).findAsync(bootstrap);
71
+ expect(result.ok).toBe(false);
72
+ expect(result.diagnostics).toEqual([
73
+ {
74
+ severity: 'error',
75
+ code: 'infra-control-plane-credential-store-invalid',
76
+ message:
77
+ 'Stored control-plane credential FIXTURE_BOOTSTRAP has an invalid project-state shape.',
78
+ },
79
+ ]);
80
+ });
81
+ });
82
+
83
+ it('uses restrictive permissions and leaves no partial files after concurrent writes', async () => {
84
+ await withTempProjectAsync(async (projectPath) => {
85
+ const port = createPort(projectPath);
86
+ await Promise.all([
87
+ port.persistAsync(bootstrap, generated),
88
+ port.persistAsync(secondary, { token: 'another-secret' }),
89
+ ]);
90
+
91
+ const directory = path.dirname(credentialPath(projectPath, 'local', bootstrap.name));
92
+ const entries = await fs.readdir(directory);
93
+ expect(entries.filter((entry) => entry.endsWith('.json'))).toHaveLength(2);
94
+ expect(entries.some((entry) => entry.endsWith('.tmp'))).toBe(false);
95
+ if (process.platform !== 'win32') {
96
+ expect((await fs.stat(directory)).mode & 0o777).toBe(0o700);
97
+ expect(
98
+ (await fs.stat(credentialPath(projectPath, 'local', bootstrap.name))).mode & 0o777,
99
+ ).toBe(0o600);
100
+ }
101
+ });
102
+ });
103
+
104
+ it('isolates persisted credentials by project and environment', async () => {
105
+ await withTempProjectAsync(async (root) => {
106
+ const projectA = path.join(root, 'a');
107
+ const projectB = path.join(root, 'b');
108
+ const local = createPort(projectA);
109
+ const preview = createPort(projectA, {}, 'preview');
110
+ const otherProject = createPort(projectB);
111
+ await local.persistAsync(bootstrap, generated);
112
+
113
+ expect(await local.resolveAsync(bootstrap)).toMatchObject({ ok: true, value: generated });
114
+ expect(await preview.findAsync(bootstrap)).toEqual({ ok: true, value: null, diagnostics: [] });
115
+ expect(await otherProject.findAsync(bootstrap)).toEqual({
116
+ ok: true,
117
+ value: null,
118
+ diagnostics: [],
119
+ });
120
+ });
121
+ });
122
+
123
+ function createPort(
124
+ projectPath: string,
125
+ processEnvironment: Readonly<Record<string, string | undefined>> = {},
126
+ environment: 'local' | 'preview' = 'local',
127
+ ) {
128
+ return createProjectInfraCredentialPort({ projectPath, environment, processEnvironment });
129
+ }
130
+
131
+ function reference(name: string): InfraControlPlaneCredentialRef {
132
+ return { source: 'control-plane', name };
133
+ }
134
+
135
+ function credentialPath(
136
+ projectPath: string,
137
+ environment: 'local' | 'preview',
138
+ name: string,
139
+ ): string {
140
+ const digest = createHash('sha256').update(name).digest('hex');
141
+ return path.join(projectPath, '.ankh', 'infra', environment, 'credentials', `${digest}.json`);
142
+ }
143
+
144
+ async function withTempProjectAsync(run: (projectPath: string) => Promise<void>): Promise<void> {
145
+ const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ankh-infra-credentials-'));
146
+ try {
147
+ await run(projectPath);
148
+ } finally {
149
+ await fs.rm(projectPath, { recursive: true, force: true });
150
+ }
151
+ }
@@ -3,6 +3,7 @@ import type { AppEnvironmentId } from '@ankhorage/contracts/environments';
3
3
  import type { InfraEnvironmentSpec } from '@ankhorage/contracts/infra';
4
4
 
5
5
  import type { readStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/readStoredInfraStateAsync.js';
6
+ import type { removeStoredInfraCredentialsAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js';
6
7
  import type { removeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/removeStoredInfraStateAsync.js';
7
8
  import type { writeInfraGeneratedArtifactsAsync } from '../features/environment-lifecycle/adapters/outbound/writeInfraGeneratedArtifactsAsync.js';
8
9
  import type { writeStoredInfraStateAsync } from '../features/environment-lifecycle/adapters/outbound/writeStoredInfraStateAsync.js';
@@ -79,6 +80,11 @@ export interface InfraLifecycleOperations {
79
80
  readonly destroy: typeof destroyInfraEnvironmentAsync;
80
81
  }
81
82
 
83
+ interface InfraDependencyScope {
84
+ readonly projectPath: string;
85
+ readonly environment: AppEnvironmentId;
86
+ }
87
+
82
88
  export interface InfraCommandServices {
83
89
  readonly resolveProject: (options: {
84
90
  readonly cwd: string;
@@ -87,9 +93,13 @@ export interface InfraCommandServices {
87
93
  readonly readState: typeof readStoredInfraStateAsync;
88
94
  readonly writeState: typeof writeStoredInfraStateAsync;
89
95
  readonly removeState: typeof removeStoredInfraStateAsync;
96
+ readonly removeCredentials: typeof removeStoredInfraCredentialsAsync;
90
97
  readonly writeArtifacts: typeof writeInfraGeneratedArtifactsAsync;
91
98
  readonly operations: InfraLifecycleOperations;
92
- readonly createDependencies: (context: InfraCommandContext) => InfraOrchestrationDependencies;
99
+ readonly createDependencies: (
100
+ context: InfraCommandContext,
101
+ scope: InfraDependencyScope,
102
+ ) => InfraOrchestrationDependencies;
93
103
  }
94
104
 
95
105
  export interface RunInfraCommandOptions {