@ankhorage/doctor 0.5.0 → 0.7.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.
@@ -0,0 +1,241 @@
1
+ import {
2
+ findForbiddenInlineSecretFields,
3
+ normalizeSecretRef,
4
+ SECRET_STORE_PROVIDERS,
5
+ } from '@ankhorage/contracts/secrets';
6
+
7
+ import type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';
8
+
9
+ const PROFILE = 'app-manifest' as const;
10
+
11
+ export function analyzeSecretStoreManifest(
12
+ infra: Record<string, unknown>,
13
+ manifestPath: string,
14
+ ): DoctorDiagnostic[] {
15
+ const diagnostics: DoctorDiagnostic[] = [];
16
+ const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);
17
+
18
+ if (!isRecord(infra.auth)) return diagnostics;
19
+ const { oauth } = infra.auth;
20
+ if (oauth === undefined) return diagnostics;
21
+
22
+ if (!isRecord(oauth)) {
23
+ diagnostics.push(
24
+ diagnostic(
25
+ 'field-invalid',
26
+ 'manifest.infra.auth.oauth must be a JSON object when present.',
27
+ manifestPath,
28
+ 'manifest.auth.oauth.valid-shape',
29
+ ),
30
+ );
31
+ return diagnostics;
32
+ }
33
+
34
+ if (typeof oauth.enabled !== 'boolean') {
35
+ diagnostics.push(
36
+ diagnostic(
37
+ oauth.enabled === undefined ? 'field-missing' : 'field-invalid',
38
+ 'manifest.infra.auth.oauth.enabled must be a boolean.',
39
+ manifestPath,
40
+ 'manifest.auth.oauth.valid-shape',
41
+ ),
42
+ );
43
+ }
44
+
45
+ if (
46
+ typeof oauth.callbackRoute !== 'string' ||
47
+ oauth.callbackRoute.trim().length === 0 ||
48
+ !oauth.callbackRoute.trim().startsWith('/')
49
+ ) {
50
+ diagnostics.push(
51
+ diagnostic(
52
+ oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid',
53
+ 'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.',
54
+ manifestPath,
55
+ 'manifest.auth.oauth.callback-route.valid',
56
+ ),
57
+ );
58
+ }
59
+
60
+ if (!Array.isArray(oauth.providers)) {
61
+ diagnostics.push(
62
+ diagnostic(
63
+ oauth.providers === undefined ? 'field-missing' : 'field-invalid',
64
+ 'manifest.infra.auth.oauth.providers must be an array.',
65
+ manifestPath,
66
+ 'manifest.auth.oauth.providers.valid',
67
+ ),
68
+ );
69
+ return diagnostics;
70
+ }
71
+
72
+ const enabledProviders = oauth.providers.filter(
73
+ (provider) => isRecord(provider) && provider.enabled !== false,
74
+ );
75
+
76
+ if (oauth.enabled === true && enabledProviders.length === 0) {
77
+ diagnostics.push(
78
+ diagnostic(
79
+ 'field-invalid',
80
+ 'OAuth is enabled, but no OAuth providers are enabled.',
81
+ manifestPath,
82
+ 'manifest.auth.oauth.providers.valid',
83
+ ),
84
+ );
85
+ }
86
+
87
+ if (enabledProviders.length > 0 && secretStoreProvider === null) {
88
+ diagnostics.push(
89
+ diagnostic(
90
+ 'field-missing',
91
+ 'Enabled OAuth providers require manifest.infra.secretStore.provider.',
92
+ manifestPath,
93
+ 'manifest.secret-store.provider.required',
94
+ ),
95
+ );
96
+ }
97
+
98
+ const seenProviderIds = new Set<string>();
99
+
100
+ for (const [index, provider] of oauth.providers.entries()) {
101
+ if (!isRecord(provider)) {
102
+ diagnostics.push(
103
+ diagnostic(
104
+ 'field-invalid',
105
+ `manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`,
106
+ manifestPath,
107
+ 'manifest.auth.oauth.providers.valid',
108
+ ),
109
+ );
110
+ continue;
111
+ }
112
+
113
+ const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';
114
+ if (providerId.length === 0) {
115
+ diagnostics.push(
116
+ diagnostic(
117
+ provider.id === undefined ? 'field-missing' : 'field-invalid',
118
+ `manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`,
119
+ manifestPath,
120
+ 'manifest.auth.oauth.providers.valid',
121
+ ),
122
+ );
123
+ } else if (seenProviderIds.has(providerId)) {
124
+ diagnostics.push(
125
+ diagnostic(
126
+ 'field-invalid',
127
+ `OAuth provider id "${providerId}" is configured more than once.`,
128
+ manifestPath,
129
+ 'manifest.auth.oauth.provider.duplicate',
130
+ ),
131
+ );
132
+ } else {
133
+ seenProviderIds.add(providerId);
134
+ }
135
+
136
+ for (const field of findForbiddenInlineSecretFields(provider)) {
137
+ diagnostics.push(
138
+ diagnostic(
139
+ 'field-invalid',
140
+ `Inline OAuth field "${field}" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`,
141
+ manifestPath,
142
+ 'manifest.auth.oauth.provider.inline-secret.disallowed',
143
+ ),
144
+ );
145
+ }
146
+
147
+ if (provider.enabled === false) continue;
148
+
149
+ if (typeof provider.credentialsRef !== 'string') {
150
+ diagnostics.push(
151
+ diagnostic(
152
+ provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid',
153
+ `Enabled OAuth provider "${providerId || index}" requires a credentialsRef.`,
154
+ manifestPath,
155
+ 'manifest.auth.oauth.provider.credentials-ref.required',
156
+ ),
157
+ );
158
+ continue;
159
+ }
160
+
161
+ const refResult = normalizeSecretRef(provider.credentialsRef);
162
+ if (!refResult.ok || refResult.data !== provider.credentialsRef) {
163
+ diagnostics.push(
164
+ diagnostic(
165
+ 'field-invalid',
166
+ `OAuth provider "${providerId || index}" credentialsRef must be a canonical logical secret reference.`,
167
+ manifestPath,
168
+ 'manifest.auth.oauth.provider.credentials-ref.valid',
169
+ ),
170
+ );
171
+ }
172
+ }
173
+
174
+ return diagnostics;
175
+ }
176
+
177
+ function validateSecretStore(
178
+ value: unknown,
179
+ manifestPath: string,
180
+ diagnostics: DoctorDiagnostic[],
181
+ ): string | null {
182
+ if (value === undefined) return null;
183
+
184
+ if (!isRecord(value)) {
185
+ diagnostics.push(
186
+ diagnostic(
187
+ 'field-invalid',
188
+ 'manifest.infra.secretStore must be a JSON object when present.',
189
+ manifestPath,
190
+ 'manifest.secret-store.valid-shape',
191
+ ),
192
+ );
193
+ return null;
194
+ }
195
+
196
+ if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {
197
+ diagnostics.push(
198
+ diagnostic(
199
+ value.provider === undefined ? 'field-missing' : 'field-invalid',
200
+ 'manifest.infra.secretStore.provider must be a non-empty string.',
201
+ manifestPath,
202
+ 'manifest.secret-store.provider.required',
203
+ ),
204
+ );
205
+ return null;
206
+ }
207
+
208
+ const provider = value.provider.trim();
209
+ if (!(SECRET_STORE_PROVIDERS as readonly string[]).includes(provider)) {
210
+ diagnostics.push(
211
+ diagnostic(
212
+ 'field-invalid',
213
+ `Unsupported secret-store provider "${provider}". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`,
214
+ manifestPath,
215
+ 'manifest.secret-store.provider.valid',
216
+ ),
217
+ );
218
+ }
219
+
220
+ return provider;
221
+ }
222
+
223
+ function diagnostic(
224
+ code: DoctorDiagnostic['code'],
225
+ message: string,
226
+ path: string,
227
+ ruleId: DoctorRuleId,
228
+ ): DoctorDiagnostic {
229
+ return {
230
+ code,
231
+ message,
232
+ path,
233
+ profile: PROFILE,
234
+ ruleId,
235
+ severity: 'error',
236
+ };
237
+ }
238
+
239
+ function isRecord(value: unknown): value is Record<string, unknown> {
240
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
241
+ }