@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.
- package/CHANGELOG.md +12 -0
- package/dist/cli/commands/destroy.d.ts.map +1 -1
- package/dist/cli/commands/destroy.js +1 -0
- package/dist/cli/commands/destroy.js.map +1 -1
- package/dist/cli/utils/prepareInfraCommandOperationAsync.d.ts.map +1 -1
- package/dist/cli/utils/prepareInfraCommandOperationAsync.js +4 -1
- package/dist/cli/utils/prepareInfraCommandOperationAsync.js.map +1 -1
- package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.d.ts +11 -0
- package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.d.ts.map +1 -0
- package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.js +125 -0
- package/dist/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.js.map +1 -0
- package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.d.ts +4 -0
- package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.d.ts.map +1 -0
- package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js +9 -0
- package/dist/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.js.map +1 -0
- package/dist/features/environment-lifecycle/composition/createInfraCommandServices.d.ts.map +1 -1
- package/dist/features/environment-lifecycle/composition/createInfraCommandServices.js +9 -3
- package/dist/features/environment-lifecycle/composition/createInfraCommandServices.js.map +1 -1
- package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.d.ts.map +1 -1
- package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.js +28 -2
- package/dist/features/environment-lifecycle/composition/createProjectInfraLifecycle.js.map +1 -1
- package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.d.ts +4 -0
- package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.d.ts.map +1 -0
- package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.js +11 -0
- package/dist/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.js.map +1 -0
- package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.d.ts +1 -1
- package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.d.ts.map +1 -1
- package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.js +2 -6
- package/dist/features/environment-lifecycle/utils/resolveInfraStatePath.js.map +1 -1
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.d.ts.map +1 -1
- package/dist/project/index.js +1 -0
- package/dist/project/index.js.map +1 -1
- package/dist/types/infraCli.d.ts +7 -1
- package/dist/types/infraCli.d.ts.map +1 -1
- package/dist/types/infraCli.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/commands/destroy.ts +1 -0
- package/src/cli/utils/prepareInfraCommandOperationAsync.ts +4 -1
- package/src/features/environment-lifecycle/adapters/outbound/createProjectInfraCredentialPort.ts +182 -0
- package/src/features/environment-lifecycle/adapters/outbound/removeStoredInfraCredentialsAsync.ts +18 -0
- package/src/features/environment-lifecycle/composition/createInfraCommandServices.ts +9 -3
- package/src/features/environment-lifecycle/composition/createProjectInfraLifecycle.ts +33 -4
- package/src/features/environment-lifecycle/utils/resolveInfraEnvironmentStateDirectory.ts +16 -0
- package/src/features/environment-lifecycle/utils/resolveInfraStatePath.ts +3 -6
- package/src/hetznerK3sProduction.test.ts +29 -3
- package/src/hetznerK3sSupabaseR2.test.ts +33 -4
- package/src/localDockerComposeProviders.integration.test.ts +90 -51
- package/src/localK3sSupabase.test.ts +85 -27
- package/src/localMinikubeSupabase.integration.test.ts +175 -122
- package/src/localMinikubeSupabaseRecovery.integration.test.ts +37 -10
- package/src/project/index.test.ts +3 -1
- package/src/project/index.ts +1 -0
- package/src/projectCredentialLifecycle.test.ts +161 -0
- package/src/projectCredentialStore.test.ts +151 -0
- package/src/types/infraCli.ts +11 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
InfraControlPlaneCredentialRef,
|
|
5
|
+
InfraCredentialPort,
|
|
4
6
|
InfraLedger,
|
|
5
7
|
InfraManifest,
|
|
6
8
|
InfraResourceIdentity,
|
|
@@ -19,16 +21,7 @@ import type { InfraOrchestrationDependencies } from './types/infraOrchestration.
|
|
|
19
21
|
const projectId = 'infra145-compose-providers';
|
|
20
22
|
const baseUrl = 'http://127.0.0.1:54322';
|
|
21
23
|
const bucket = 'phase9-acceptance';
|
|
22
|
-
const
|
|
23
|
-
const credentials = {
|
|
24
|
-
postgresPassword: 'phase9-postgres-password',
|
|
25
|
-
jwtSecret,
|
|
26
|
-
anonKey: createJwt('anon'),
|
|
27
|
-
serviceRoleKey: createJwt('service_role'),
|
|
28
|
-
realtimeSecretKeyBase: 'r'.repeat(64),
|
|
29
|
-
realtimeDatabaseEncryptionKey: '0123456789abcdef',
|
|
30
|
-
pgMetaCryptoKey: 'phase9-meta-crypto-key-that-is-at-least-32-characters',
|
|
31
|
-
} as const;
|
|
24
|
+
const bootstrapCredential = { source: 'control-plane', name: 'SUPABASE_BOOTSTRAP' } as const;
|
|
32
25
|
const manifest = {
|
|
33
26
|
environments: {
|
|
34
27
|
local: {
|
|
@@ -59,7 +52,8 @@ const manifest = {
|
|
|
59
52
|
test.skipIf(process.env.ANKH_INFRA_DOCKER_COMPOSE_PROVIDERS_E2E !== '1')(
|
|
60
53
|
'runs Supabase and Cerbos through the provider-neutral Docker Compose lifecycle',
|
|
61
54
|
async () => {
|
|
62
|
-
const
|
|
55
|
+
const credentialPort = new MemoryCredentialPort();
|
|
56
|
+
const dependencies = createDependencies(credentialPort);
|
|
63
57
|
let ledger: InfraLedger | undefined;
|
|
64
58
|
|
|
65
59
|
try {
|
|
@@ -72,7 +66,10 @@ test.skipIf(process.env.ANKH_INFRA_DOCKER_COMPOSE_PROVIDERS_E2E !== '1')(
|
|
|
72
66
|
await upInfraEnvironmentAsync({ projectId, manifest }, dependencies),
|
|
73
67
|
);
|
|
74
68
|
ledger = firstLedger;
|
|
75
|
-
|
|
69
|
+
expect(credentialPort.persistCount).toBe(1);
|
|
70
|
+
const firstCredentials = credentialPort.readRequired();
|
|
71
|
+
const firstCredentialFingerprint = credentialFingerprint(firstCredentials);
|
|
72
|
+
assertProviderNeutralOutputs(firstOutputs, firstCredentials);
|
|
76
73
|
|
|
77
74
|
const status = requireSuccess(
|
|
78
75
|
await statusInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
@@ -98,7 +95,9 @@ test.skipIf(process.env.ANKH_INFRA_DOCKER_COMPOSE_PROVIDERS_E2E !== '1')(
|
|
|
98
95
|
await upInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
99
96
|
);
|
|
100
97
|
ledger = resumedLedger;
|
|
101
|
-
|
|
98
|
+
expect(credentialPort.persistCount).toBe(1);
|
|
99
|
+
expect(credentialFingerprint(credentialPort.readRequired())).toBe(firstCredentialFingerprint);
|
|
100
|
+
assertProviderNeutralOutputs(resumedOutputs, firstCredentials);
|
|
102
101
|
|
|
103
102
|
const health = await fetch(`${baseUrl}/auth/v1/health`);
|
|
104
103
|
expect(health.ok).toBe(true);
|
|
@@ -123,20 +122,10 @@ test.skipIf(process.env.ANKH_INFRA_DOCKER_COMPOSE_PROVIDERS_E2E !== '1')(
|
|
|
123
122
|
900_000,
|
|
124
123
|
);
|
|
125
124
|
|
|
126
|
-
function createDependencies(): InfraOrchestrationDependencies {
|
|
125
|
+
function createDependencies(credentials: InfraCredentialPort): InfraOrchestrationDependencies {
|
|
127
126
|
return {
|
|
128
127
|
adapterResolver: createNodeInfraAdapterPackageResolver(),
|
|
129
|
-
credentials
|
|
130
|
-
resolveAsync: (reference) =>
|
|
131
|
-
Promise.resolve(
|
|
132
|
-
reference.name === 'SUPABASE_BOOTSTRAP'
|
|
133
|
-
? success(credentials)
|
|
134
|
-
: failure(
|
|
135
|
-
'unexpected-control-plane-credential',
|
|
136
|
-
`Unexpected control-plane credential ${reference.name}.`,
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
},
|
|
128
|
+
credentials,
|
|
140
129
|
secrets: {
|
|
141
130
|
resolveAsync: (reference) =>
|
|
142
131
|
Promise.resolve(
|
|
@@ -149,6 +138,73 @@ function createDependencies(): InfraOrchestrationDependencies {
|
|
|
149
138
|
};
|
|
150
139
|
}
|
|
151
140
|
|
|
141
|
+
class MemoryCredentialPort implements InfraCredentialPort {
|
|
142
|
+
private values: Readonly<Record<string, string>> | null = null;
|
|
143
|
+
persistCount = 0;
|
|
144
|
+
|
|
145
|
+
findAsync(
|
|
146
|
+
reference: InfraControlPlaneCredentialRef,
|
|
147
|
+
): Promise<InfraResult<Readonly<Record<string, string>> | null>> {
|
|
148
|
+
if (reference.name !== bootstrapCredential.name) return Promise.resolve(success(null));
|
|
149
|
+
return Promise.resolve(success(this.values === null ? null : { ...this.values }));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
resolveAsync(
|
|
153
|
+
reference: InfraControlPlaneCredentialRef,
|
|
154
|
+
): Promise<InfraResult<Readonly<Record<string, string>>>> {
|
|
155
|
+
if (reference.name === bootstrapCredential.name && this.values !== null) {
|
|
156
|
+
return Promise.resolve(success({ ...this.values }));
|
|
157
|
+
}
|
|
158
|
+
return Promise.resolve(
|
|
159
|
+
failure(
|
|
160
|
+
'unexpected-control-plane-credential',
|
|
161
|
+
`Unexpected or missing control-plane credential ${reference.name}.`,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
persistAsync(
|
|
167
|
+
reference: InfraControlPlaneCredentialRef,
|
|
168
|
+
values: Readonly<Record<string, string>>,
|
|
169
|
+
): Promise<InfraResult<null>> {
|
|
170
|
+
if (reference.name !== bootstrapCredential.name) {
|
|
171
|
+
return Promise.resolve(
|
|
172
|
+
failure(
|
|
173
|
+
'unexpected-control-plane-credential',
|
|
174
|
+
`Unexpected control-plane credential ${reference.name}.`,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
this.values = { ...values };
|
|
179
|
+
this.persistCount += 1;
|
|
180
|
+
return Promise.resolve(success(null));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
readRequired(): Readonly<Record<string, string>> {
|
|
184
|
+
if (this.values === null) throw new Error('Expected generated Supabase bootstrap credentials.');
|
|
185
|
+
return { ...this.values };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function credentialFingerprint(values: Readonly<Record<string, string>>): string {
|
|
190
|
+
const canonical = Object.entries(values)
|
|
191
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
192
|
+
.map(([key, value]) => `${key}\u0000${value}`)
|
|
193
|
+
.join('\u0001');
|
|
194
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function privilegedCredentialValues(values: Readonly<Record<string, string>>): readonly string[] {
|
|
198
|
+
const { anonKey: _anonKey, ...privileged } = values;
|
|
199
|
+
return Object.values(privileged);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readAnonKey(values: Readonly<Record<string, string>>): string {
|
|
203
|
+
const { anonKey } = values;
|
|
204
|
+
if (anonKey === undefined) throw new Error('Expected generated Supabase anonKey.');
|
|
205
|
+
return anonKey;
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
function createDestroyRequest(
|
|
153
209
|
previous: InfraLedger,
|
|
154
210
|
confirmedResources: readonly InfraResourceIdentity[],
|
|
@@ -167,14 +223,14 @@ function persistentIdentities(ledger: InfraLedger): readonly InfraResourceIdenti
|
|
|
167
223
|
return ledger.resources.filter(({ persistent }) => persistent).map(({ identity }) => identity);
|
|
168
224
|
}
|
|
169
225
|
|
|
170
|
-
function assertProviderNeutralOutputs(
|
|
226
|
+
function assertProviderNeutralOutputs(
|
|
227
|
+
outputs: InfraLedger['outputs'],
|
|
228
|
+
credentials: Readonly<Record<string, string>>,
|
|
229
|
+
): void {
|
|
171
230
|
const serialized = JSON.stringify(outputs);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(serialized).not.toContain(credentials.realtimeSecretKeyBase);
|
|
176
|
-
expect(serialized).not.toContain(credentials.realtimeDatabaseEncryptionKey);
|
|
177
|
-
expect(serialized).not.toContain(credentials.pgMetaCryptoKey);
|
|
231
|
+
for (const secret of privilegedCredentialValues(credentials)) {
|
|
232
|
+
expect(serialized).not.toContain(secret);
|
|
233
|
+
}
|
|
178
234
|
expect(serialized.toLowerCase()).not.toContain('kubernetes');
|
|
179
235
|
expect(serialized.toLowerCase()).not.toContain('minikube');
|
|
180
236
|
expect(
|
|
@@ -190,7 +246,7 @@ function assertProviderNeutralOutputs(outputs: InfraLedger['outputs']): void {
|
|
|
190
246
|
({ owner, environmentVariable, value }) =>
|
|
191
247
|
owner.adapter === 'supabase' &&
|
|
192
248
|
environmentVariable === 'EXPO_PUBLIC_SUPABASE_ANON_KEY' &&
|
|
193
|
-
value === credentials
|
|
249
|
+
value === readAnonKey(credentials),
|
|
194
250
|
),
|
|
195
251
|
).toBe(true);
|
|
196
252
|
expect(
|
|
@@ -209,23 +265,6 @@ function assertProviderNeutralOutputs(outputs: InfraLedger['outputs']): void {
|
|
|
209
265
|
).toBe(true);
|
|
210
266
|
}
|
|
211
267
|
|
|
212
|
-
function createJwt(role: 'anon' | 'service_role'): string {
|
|
213
|
-
const encodedHeader = encodeJwtPart({ alg: 'HS256', typ: 'JWT' });
|
|
214
|
-
const encodedPayload = encodeJwtPart({
|
|
215
|
-
role,
|
|
216
|
-
iss: 'supabase',
|
|
217
|
-
iat: 1_700_000_000,
|
|
218
|
-
exp: 4_102_444_800,
|
|
219
|
-
});
|
|
220
|
-
const unsigned = `${encodedHeader}.${encodedPayload}`;
|
|
221
|
-
const signature = createHmac('sha256', jwtSecret).update(unsigned).digest('base64url');
|
|
222
|
-
return `${unsigned}.${signature}`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function encodeJwtPart(value: Readonly<Record<string, string | number>>): string {
|
|
226
|
-
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
268
|
function requireSuccess<T>(result: InfraResult<T>): T {
|
|
230
269
|
if (!result.ok) throw new Error(result.diagnostics.map(({ message }) => message).join(' '));
|
|
231
270
|
return result.value;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
InfraComputeTarget,
|
|
5
|
+
InfraControlPlaneCredentialRef,
|
|
6
|
+
InfraCredentialPort,
|
|
3
7
|
InfraLedger,
|
|
4
8
|
InfraManifest,
|
|
5
9
|
InfraResourceIdentity,
|
|
@@ -38,6 +42,7 @@ import type { InfraOrchestrationDependencies } from './types/infraOrchestration.
|
|
|
38
42
|
const projectId = 'infra164-local-k3s-supabase';
|
|
39
43
|
const publicBaseUrl = 'http://127.0.0.1:54321';
|
|
40
44
|
const bucket = 'infra164-bucket';
|
|
45
|
+
const bootstrapCredential = { source: 'control-plane', name: 'SUPABASE_BOOTSTRAP' } as const;
|
|
41
46
|
const target: Extract<InfraComputeTarget, { kind: 'local-host' }> = {
|
|
42
47
|
id: 'local',
|
|
43
48
|
kind: 'local-host',
|
|
@@ -73,6 +78,10 @@ test('runs local k3s and Supabase through the portable compute lifecycle', async
|
|
|
73
78
|
await upInfraEnvironmentAsync({ projectId, manifest }, dependencies),
|
|
74
79
|
);
|
|
75
80
|
const firstLedger: InfraLedger = firstUp.ledger;
|
|
81
|
+
expect(fixture.credentials.persistCount).toBe(1);
|
|
82
|
+
const firstCredentials = fixture.credentials.readRequired();
|
|
83
|
+
const firstCredentialFingerprint = credentialFingerprint(firstCredentials);
|
|
84
|
+
assertPrivilegedValuesAbsent(firstUp, firstCredentials);
|
|
76
85
|
expect(firstUp.targets).toEqual([target]);
|
|
77
86
|
expect([...new Set(fixture.loadedPackages)].sort()).toEqual(
|
|
78
87
|
[
|
|
@@ -88,8 +97,6 @@ test('runs local k3s and Supabase through the portable compute lifecycle', async
|
|
|
88
97
|
expect(fixture.kubernetes.serializedResources()).toContain('/etc/postgresql-custom');
|
|
89
98
|
expect(fixture.kubernetes.serializedResources()).toContain('.ankhorage-image-seeded');
|
|
90
99
|
expect(fixture.supabase.buckets.has(bucket)).toBe(true);
|
|
91
|
-
expect(JSON.stringify(firstUp)).not.toContain('infra164-service-role-key');
|
|
92
|
-
expect(JSON.stringify(firstUp)).not.toContain('infra164-postgres-password');
|
|
93
100
|
|
|
94
101
|
const outputs = requireSuccess(
|
|
95
102
|
getInfraEnvironmentOutputs({
|
|
@@ -144,6 +151,11 @@ test('runs local k3s and Supabase through the portable compute lifecycle', async
|
|
|
144
151
|
expect(fixture.k3s.state).toBe('ready');
|
|
145
152
|
expect(resumed.targets).toEqual([target]);
|
|
146
153
|
expect(resourceIdentities(resumed.resources)).toEqual(identitiesBeforeRestart);
|
|
154
|
+
expect(fixture.credentials.persistCount).toBe(1);
|
|
155
|
+
expect(credentialFingerprint(fixture.credentials.readRequired())).toBe(
|
|
156
|
+
firstCredentialFingerprint,
|
|
157
|
+
);
|
|
158
|
+
assertPrivilegedValuesAbsent(resumed, firstCredentials);
|
|
147
159
|
|
|
148
160
|
const destroyed = requireSuccess(
|
|
149
161
|
await destroyInfraEnvironmentAsync(
|
|
@@ -171,6 +183,7 @@ class LocalK3sFixture {
|
|
|
171
183
|
readonly k3s = new FakeK3sControlPlane();
|
|
172
184
|
readonly kubernetes = this.k3s.api;
|
|
173
185
|
readonly supabase = new FakeSupabaseControlPlane();
|
|
186
|
+
readonly credentials = new MemoryCredentialPort();
|
|
174
187
|
readonly loadedPackages: string[] = [];
|
|
175
188
|
}
|
|
176
189
|
|
|
@@ -201,9 +214,7 @@ function createDependencies(fixture: LocalK3sFixture): InfraOrchestrationDepende
|
|
|
201
214
|
}
|
|
202
215
|
},
|
|
203
216
|
},
|
|
204
|
-
credentials:
|
|
205
|
-
resolveAsync: ({ name }) => Promise.resolve(resolveCredential(name)),
|
|
206
|
-
},
|
|
217
|
+
credentials: fixture.credentials,
|
|
207
218
|
secrets: {
|
|
208
219
|
resolveAsync: () =>
|
|
209
220
|
Promise.resolve({
|
|
@@ -216,29 +227,72 @@ function createDependencies(fixture: LocalK3sFixture): InfraOrchestrationDepende
|
|
|
216
227
|
};
|
|
217
228
|
}
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
class MemoryCredentialPort implements InfraCredentialPort {
|
|
231
|
+
private values: Readonly<Record<string, string>> | null = null;
|
|
232
|
+
persistCount = 0;
|
|
233
|
+
|
|
234
|
+
findAsync(
|
|
235
|
+
reference: InfraControlPlaneCredentialRef,
|
|
236
|
+
): Promise<InfraResult<Readonly<Record<string, string>> | null>> {
|
|
237
|
+
if (reference.name !== bootstrapCredential.name) return Promise.resolve(success(null));
|
|
238
|
+
return Promise.resolve(success(this.values === null ? null : { ...this.values }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resolveAsync(
|
|
242
|
+
reference: InfraControlPlaneCredentialRef,
|
|
243
|
+
): Promise<InfraResult<Readonly<Record<string, string>>>> {
|
|
244
|
+
if (reference.name === bootstrapCredential.name && this.values !== null) {
|
|
245
|
+
return Promise.resolve(success({ ...this.values }));
|
|
246
|
+
}
|
|
247
|
+
return Promise.resolve(
|
|
248
|
+
failure(
|
|
249
|
+
'unexpected-credential',
|
|
250
|
+
`Unexpected or missing credential reference: ${reference.name}`,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
persistAsync(
|
|
256
|
+
reference: InfraControlPlaneCredentialRef,
|
|
257
|
+
values: Readonly<Record<string, string>>,
|
|
258
|
+
): Promise<InfraResult<null>> {
|
|
259
|
+
if (reference.name !== bootstrapCredential.name) {
|
|
260
|
+
return Promise.resolve(
|
|
261
|
+
failure('unexpected-credential', `Unexpected credential reference: ${reference.name}`),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this.values = { ...values };
|
|
265
|
+
this.persistCount += 1;
|
|
266
|
+
return Promise.resolve(success(null));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
readRequired(): Readonly<Record<string, string>> {
|
|
270
|
+
if (this.values === null) throw new Error('Expected generated Supabase bootstrap credentials.');
|
|
271
|
+
return { ...this.values };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function credentialFingerprint(values: Readonly<Record<string, string>>): string {
|
|
276
|
+
const canonical = Object.entries(values)
|
|
277
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
278
|
+
.map(([key, value]) => `${key}\u0000${value}`)
|
|
279
|
+
.join('\u0001');
|
|
280
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function privilegedCredentialValues(values: Readonly<Record<string, string>>): readonly string[] {
|
|
284
|
+
const { anonKey: _anonKey, ...privileged } = values;
|
|
285
|
+
return Object.values(privileged);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function assertPrivilegedValuesAbsent(
|
|
289
|
+
value: unknown,
|
|
290
|
+
credentials: Readonly<Record<string, string>>,
|
|
291
|
+
): void {
|
|
292
|
+
const serialized = JSON.stringify(value);
|
|
293
|
+
for (const secret of privilegedCredentialValues(credentials)) {
|
|
294
|
+
expect(serialized).not.toContain(secret);
|
|
231
295
|
}
|
|
232
|
-
return {
|
|
233
|
-
ok: false,
|
|
234
|
-
diagnostics: [
|
|
235
|
-
{
|
|
236
|
-
severity: 'error',
|
|
237
|
-
code: 'unexpected-credential',
|
|
238
|
-
message: `Unexpected credential reference: ${name}`,
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
};
|
|
242
296
|
}
|
|
243
297
|
|
|
244
298
|
class FakeLocalHostProbe implements LocalHostProbe {
|
|
@@ -446,3 +500,7 @@ function requireSuccess<T>(result: InfraResult<T>): T {
|
|
|
446
500
|
function success<T>(value: T): InfraResult<T> {
|
|
447
501
|
return { ok: true, value, diagnostics: [] };
|
|
448
502
|
}
|
|
503
|
+
|
|
504
|
+
function failure(code: string, message: string): InfraResult<never> {
|
|
505
|
+
return { ok: false, diagnostics: [{ severity: 'error', code, message }] };
|
|
506
|
+
}
|