@pikku/core 0.12.77 → 0.12.79

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.
@@ -32,6 +32,134 @@ function parseIPv4Octets(
32
32
  return octets as [number, number, number, number]
33
33
  }
34
34
 
35
+ /**
36
+ * IPv4 blocks that must never be reachable from user-supplied URLs: private,
37
+ * loopback, link-local (cloud metadata), carrier-grade NAT (Alibaba's
38
+ * `100.100.100.200` metadata endpoint), IETF protocol assignments, benchmarking,
39
+ * 6to4 anycast, the documentation TEST-NETs, multicast and reserved space.
40
+ */
41
+ const PRIVATE_IPV4_BLOCKS: ReadonlyArray<readonly [string, number]> = [
42
+ ['0.0.0.0', 8],
43
+ ['10.0.0.0', 8],
44
+ ['100.64.0.0', 10],
45
+ ['127.0.0.0', 8],
46
+ ['169.254.0.0', 16],
47
+ ['172.16.0.0', 12],
48
+ ['192.0.0.0', 24],
49
+ ['192.0.2.0', 24],
50
+ ['192.88.99.0', 24],
51
+ ['192.168.0.0', 16],
52
+ ['198.18.0.0', 15],
53
+ ['198.51.100.0', 24],
54
+ ['203.0.113.0', 24],
55
+ ['224.0.0.0', 4],
56
+ ['240.0.0.0', 4],
57
+ ]
58
+
59
+ const toUint32 = (octets: [number, number, number, number]): number =>
60
+ ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0
61
+
62
+ const PRIVATE_IPV4_RANGES = PRIVATE_IPV4_BLOCKS.map(([base, prefix]) => {
63
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0
64
+ return [(toUint32(parseIPv4Octets(base)!) & mask) >>> 0, mask] as const
65
+ })
66
+
67
+ const isPrivateIPv4 = (octets: [number, number, number, number]): boolean => {
68
+ const addr = toUint32(octets)
69
+ return PRIVATE_IPV4_RANGES.some(
70
+ ([base, mask]) => (addr & mask) >>> 0 === base
71
+ )
72
+ }
73
+
74
+ /**
75
+ * Expands an IPv6 literal into its eight 16-bit groups, handling `::` elision
76
+ * and a trailing dotted-quad. `null` when not a well-formed IPv6 literal.
77
+ */
78
+ function parseIPv6Groups(host: string): number[] | null {
79
+ const zoneless = host.split('%')[0]!
80
+ const halves = zoneless.split('::')
81
+ if (halves.length > 2) return null
82
+
83
+ const parseSide = (side: string): number[] | null => {
84
+ if (side === '') return []
85
+ const parts = side.split(':')
86
+ const groups: number[] = []
87
+ for (let i = 0; i < parts.length; i++) {
88
+ const part = parts[i]!
89
+ if (i === parts.length - 1 && part.includes('.')) {
90
+ const v4 = parseIPv4Octets(part)
91
+ if (!v4) return null
92
+ groups.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3])
93
+ continue
94
+ }
95
+ if (!/^[0-9a-f]{1,4}$/.test(part)) return null
96
+ groups.push(parseInt(part, 16))
97
+ }
98
+ return groups
99
+ }
100
+
101
+ const head = parseSide(halves[0]!)
102
+ if (head === null) return null
103
+
104
+ if (halves.length === 1) {
105
+ return head.length === 8 ? head : null
106
+ }
107
+
108
+ const tail = parseSide(halves[1]!)
109
+ if (tail === null) return null
110
+ if (head.length + tail.length > 7) return null
111
+ return [...head, ...new Array(8 - head.length - tail.length).fill(0), ...tail]
112
+ }
113
+
114
+ const embeddedIPv4 = (
115
+ hi: number,
116
+ lo: number
117
+ ): [number, number, number, number] => [
118
+ (hi >> 8) & 0xff,
119
+ hi & 0xff,
120
+ (lo >> 8) & 0xff,
121
+ lo & 0xff,
122
+ ]
123
+
124
+ function isPrivateIPv6(groups: number[]): boolean {
125
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups as [
126
+ number,
127
+ number,
128
+ number,
129
+ number,
130
+ number,
131
+ number,
132
+ number,
133
+ number,
134
+ ]
135
+
136
+ if (groups.every((g) => g === 0)) return true // :: unspecified
137
+ if (groups.slice(0, 7).every((g) => g === 0) && g7 === 1) return true // ::1
138
+
139
+ // IPv4-mapped ::ffff:0:0/96 and IPv4-compatible ::/96
140
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0) {
141
+ if (g5 === 0xffff || g5 === 0) return isPrivateIPv4(embeddedIPv4(g6, g7))
142
+ }
143
+ // NAT64 well-known prefix 64:ff9b::/96
144
+ if (
145
+ g0 === 0x64 &&
146
+ g1 === 0xff9b &&
147
+ g2 === 0 &&
148
+ g3 === 0 &&
149
+ g4 === 0 &&
150
+ g5 === 0
151
+ )
152
+ return isPrivateIPv4(embeddedIPv4(g6, g7))
153
+ // 6to4 2002::/16 carries the IPv4 address in the next 32 bits
154
+ if (g0 === 0x2002) return isPrivateIPv4(embeddedIPv4(g1, g2))
155
+ if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0) return true // discard-only 100::/64
156
+ if ((g0 & 0xffc0) === 0xfe80) return true // link-local fe80::/10
157
+ if ((g0 & 0xfe00) === 0xfc00) return true // unique-local fc00::/7
158
+ if ((g0 & 0xffc0) === 0xfec0) return true // deprecated site-local fec0::/10
159
+ if ((g0 & 0xff00) === 0xff00) return true // multicast ff00::/8
160
+ return false
161
+ }
162
+
35
163
  /**
36
164
  * Whether a hostname is an obvious internal target. Best-effort literal
37
165
  * matching only: it cannot catch a public hostname that *resolves* to a
@@ -46,31 +174,30 @@ export function isPrivateHost(hostname: string): boolean {
46
174
  return true
47
175
 
48
176
  if (host.includes(':')) {
49
- if (host === '::' || host === '::1') return true
50
- const mappedV4 = host.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)
51
- if (mappedV4) return isPrivateHost(mappedV4[1]!)
52
- const mappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)
53
- if (mappedHex) {
54
- const hi = parseInt(mappedHex[1]!, 16)
55
- const lo = parseInt(mappedHex[2]!, 16)
56
- return isPrivateHost(
57
- `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`
58
- )
59
- }
60
- if (/^fe[89ab]/.test(host)) return true // link-local fe80::/10
61
- if (host.startsWith('fc') || host.startsWith('fd')) return true // unique-local fc00::/7
62
- return false
177
+ const groups = parseIPv6Groups(host)
178
+ return groups ? isPrivateIPv6(groups) : false
63
179
  }
64
180
 
65
181
  const v4 = parseIPv4Octets(host)
66
- if (v4) {
67
- const [a, b] = v4
68
- if (a === 127 || a === 10 || a === 0) return true
69
- if (a === 169 && b === 254) return true // link-local incl. cloud metadata
70
- if (a === 172 && b >= 16 && b <= 31) return true
71
- if (a === 192 && b === 168) return true
72
- }
73
- return false
182
+ return v4 ? isPrivateIPv4(v4) : false
183
+ }
184
+
185
+ /** Resolves a hostname to the IP addresses it points at. */
186
+ export type HostResolver = (hostname: string) => Promise<string[]>
187
+
188
+ let defaultHostResolver: HostResolver | undefined
189
+
190
+ /**
191
+ * Installs the resolver {@link safeFetch} uses when a call passes no
192
+ * `resolveHost` of its own.
193
+ *
194
+ * Core cannot resolve DNS itself — Workers has no DNS API — so without a
195
+ * resolver the guard is literal-only and a public name pointing at
196
+ * `169.254.169.254` passes. Node runtimes install
197
+ * `nodeHostResolver` from `@pikku/core/node-host-resolver` at startup.
198
+ */
199
+ export function setDefaultHostResolver(resolver: HostResolver | undefined) {
200
+ defaultHostResolver = resolver
74
201
  }
75
202
 
76
203
  export interface SafeFetchOptions {
@@ -81,6 +208,50 @@ export interface SafeFetchOptions {
81
208
  allowedHosts?: string[]
82
209
  /** Maximum redirect hops to follow (each re-validated). Defaults to 3. */
83
210
  maxRedirects?: number
211
+ /**
212
+ * Resolves a hostname so a *public* name pointing at a private address is
213
+ * refused. Defaults to whatever {@link setDefaultHostResolver} installed;
214
+ * pass `null` to opt a call out of resolution entirely.
215
+ */
216
+ resolveHost?: HostResolver | null
217
+ }
218
+
219
+ /** Whether a hostname is already an IP literal, which the sync check covers. */
220
+ function isIpLiteral(hostname: string): boolean {
221
+ const host = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '')
222
+ if (host.includes(':')) return parseIPv6Groups(host.toLowerCase()) !== null
223
+ return parseIPv4Octets(host) !== null
224
+ }
225
+
226
+ /**
227
+ * Rejects a hostname that resolves to an internal address.
228
+ *
229
+ * Resolution happens once per hop and the connection is not pinned to the
230
+ * address checked, so a rebind between this check and the socket connecting is
231
+ * still possible; catching that needs a runtime-level connect hook.
232
+ */
233
+ async function assertResolvedHostAllowed(
234
+ hostname: string,
235
+ options: SafeFetchOptions
236
+ ): Promise<void> {
237
+ if (options.allowedHosts) return
238
+ const resolver =
239
+ options.resolveHost === null
240
+ ? undefined
241
+ : (options.resolveHost ?? defaultHostResolver)
242
+ if (!resolver || isIpLiteral(hostname)) return
243
+
244
+ const addresses = await resolver(hostname)
245
+ if (addresses.length === 0) {
246
+ throw new Error(`Refusing to fetch: '${hostname}' resolved to no addresses`)
247
+ }
248
+ for (const address of addresses) {
249
+ if (isPrivateHost(address)) {
250
+ throw new Error(
251
+ `Refusing to fetch from a private/internal host: '${hostname}' resolves to ${address}`
252
+ )
253
+ }
254
+ }
84
255
  }
85
256
 
86
257
  export function assertFetchableUrl(
@@ -142,7 +313,9 @@ export async function safeFetch(
142
313
  options: SafeFetchOptions = {}
143
314
  ): Promise<Response> {
144
315
  const maxRedirects = options.maxRedirects ?? 3
145
- let currentUrl = assertFetchableUrl(url, options).toString()
316
+ const initial = assertFetchableUrl(url, options)
317
+ await assertResolvedHostAllowed(initial.hostname, options)
318
+ let currentUrl = initial.toString()
146
319
  let currentInit = init
147
320
 
148
321
  for (let hop = 0; ; hop++) {
@@ -157,10 +330,12 @@ export async function safeFetch(
157
330
  if (!location || hop >= maxRedirects) {
158
331
  return response
159
332
  }
160
- const nextUrl = assertFetchableUrl(
333
+ const next = assertFetchableUrl(
161
334
  new URL(location, currentUrl).toString(),
162
335
  options
163
- ).toString()
336
+ )
337
+ await assertResolvedHostAllowed(next.hostname, options)
338
+ const nextUrl = next.toString()
164
339
  await response.body?.cancel()
165
340
  let nextInit = redirectInit(response.status, currentInit)
166
341
  if (new URL(nextUrl).origin !== new URL(currentUrl).origin) {
@@ -8,6 +8,7 @@ import {
8
8
  addGlobalPermission,
9
9
  clearPermissionsCache,
10
10
  } from '../../permissions.js'
11
+ import { addTagMiddleware } from '../../middleware-runner.js'
11
12
  import type {
12
13
  GatewayAdapter,
13
14
  GatewayInboundMessage,
@@ -84,6 +85,31 @@ const seedCompiledMeta = () => {
84
85
  }
85
86
  }
86
87
 
88
+ /**
89
+ * What the inspector records for a gateway and the function it was wired with.
90
+ * These tests wire gateways by hand, so nothing populates it otherwise — and it
91
+ * has to be in place before `wireGateway`, exactly as the generated bootstrap
92
+ * loads every meta file before any wiring file.
93
+ */
94
+ const seedDeclaredHandler = (
95
+ gatewayName: string,
96
+ funcId: string,
97
+ funcMeta: Record<string, any>,
98
+ gatewayMeta: Record<string, any> = {}
99
+ ) => {
100
+ ;(pikkuState(null, 'function', 'meta') as any)[funcId] = {
101
+ pikkuFuncId: funcId,
102
+ inputSchemaName: null,
103
+ outputSchemaName: null,
104
+ ...funcMeta,
105
+ }
106
+ ;(pikkuState(null, 'gateway', 'meta') as any)[gatewayName] = {
107
+ pikkuFuncId: funcId,
108
+ name: gatewayName,
109
+ ...gatewayMeta,
110
+ }
111
+ }
112
+
87
113
  const postMessage = async (route: string) => {
88
114
  const request = new Request(`http://localhost${route}`, {
89
115
  method: 'POST',
@@ -229,6 +255,68 @@ describe('gateway handler authorization', () => {
229
255
  assert.deepEqual(calls, ['ran'])
230
256
  })
231
257
 
258
+ test('a handler declared with pikkuFunc keeps its session requirement', async () => {
259
+ const calls: string[] = []
260
+
261
+ seedDeclaredHandler('declared-session', 'myHandler', {
262
+ sessionless: false,
263
+ })
264
+
265
+ wireGateway({
266
+ name: 'declared-session',
267
+ type: 'webhook',
268
+ route: '/webhooks/declared-session',
269
+ adapter: createMockAdapter(),
270
+ func: {
271
+ func: async () => {
272
+ calls.push('ran')
273
+ return { text: 'reply' }
274
+ },
275
+ } as any,
276
+ })
277
+
278
+ seedCompiledMeta()
279
+ httpRouter.initialize()
280
+
281
+ const response = await postMessage('/webhooks/declared-session')
282
+
283
+ assert.equal(response.status, 403)
284
+ assert.deepEqual(
285
+ calls,
286
+ [],
287
+ 'a session-required pikkuFunc must not be silently made sessionless'
288
+ )
289
+ })
290
+
291
+ test('a handler declared sessionless keeps running without a session', async () => {
292
+ const calls: string[] = []
293
+
294
+ seedDeclaredHandler('declared-sessionless', 'mySessionlessHandler', {
295
+ sessionless: true,
296
+ })
297
+
298
+ wireGateway({
299
+ name: 'declared-sessionless',
300
+ type: 'webhook',
301
+ route: '/webhooks/declared-sessionless',
302
+ adapter: createMockAdapter(),
303
+ func: {
304
+ func: async () => {
305
+ calls.push('ran')
306
+ return { text: 'reply' }
307
+ },
308
+ } as any,
309
+ })
310
+
311
+ seedCompiledMeta()
312
+ httpRouter.initialize()
313
+
314
+ const response = await postMessage('/webhooks/declared-sessionless')
315
+
316
+ assert.equal(response.status, 200)
317
+ assert.deepEqual(calls, ['ran'])
318
+ })
319
+
232
320
  test('gateway-level auth: true requires a session', async () => {
233
321
  const calls: string[] = []
234
322
 
@@ -281,6 +369,49 @@ describe('gateway handler authorization', () => {
281
369
  assert.deepEqual(calls, [], 'auth: true must require a session')
282
370
  })
283
371
 
372
+ test('tag middleware declared for the gateway actually runs', async () => {
373
+ const order: string[] = []
374
+
375
+ addTagMiddleware('audited', [
376
+ async (_s: any, _wire: any, next: any) => {
377
+ order.push('middleware')
378
+ await next()
379
+ },
380
+ ] as any)
381
+
382
+ seedDeclaredHandler(
383
+ 'tagged',
384
+ 'myTaggedHandler',
385
+ { sessionless: true },
386
+ { tags: ['audited'], middleware: [{ type: 'tag', tag: 'audited' }] }
387
+ )
388
+
389
+ wireGateway({
390
+ name: 'tagged',
391
+ type: 'webhook',
392
+ route: '/webhooks/tagged',
393
+ adapter: createMockAdapter(),
394
+ func: {
395
+ func: async () => {
396
+ order.push('handler')
397
+ return { text: 'reply' }
398
+ },
399
+ } as any,
400
+ })
401
+
402
+ seedCompiledMeta()
403
+ httpRouter.initialize()
404
+
405
+ const response = await postMessage('/webhooks/tagged')
406
+
407
+ assert.equal(response.status, 200)
408
+ assert.deepEqual(
409
+ order,
410
+ ['middleware', 'handler'],
411
+ 'addTagMiddleware must gate a gateway carrying the tag'
412
+ )
413
+ })
414
+
284
415
  test('the adapter still auto-sends the handler reply', async () => {
285
416
  const adapter = createMockAdapter()
286
417
 
@@ -23,20 +23,46 @@ const bridgeMiddlewareSession = async (wire: PikkuRawWire): Promise<void> => {
23
23
  }
24
24
  }
25
25
 
26
+ /**
27
+ * Metadata the inspector recorded for the function the gateway was wired with.
28
+ * The bootstrap loads every meta file before any wiring file, so this is
29
+ * already populated by the time a gateway wires itself. A gateway wired by
30
+ * hand rather than through codegen has no entry, and falls back to the
31
+ * sessionless default below.
32
+ */
33
+ const declaredHandlerMeta = (config: CoreGateway) => {
34
+ const declaredFuncId = pikkuState(null, 'gateway', 'meta')[config.name]
35
+ ?.pikkuFuncId
36
+ return declaredFuncId
37
+ ? pikkuState(null, 'function', 'meta')[declaredFuncId]
38
+ : undefined
39
+ }
40
+
26
41
  // knowledge: decisions/security/gateway-handlers-run-through-the-function-runner-gate.md
27
42
  const registerGatewayHandler = (config: CoreGateway): string => {
28
43
  const funcId = gatewayHandlerFuncId(config.name)
29
44
  const funcMeta = pikkuState(null, 'function', 'meta')
45
+ const declared = declaredHandlerMeta(config)
30
46
  funcMeta[funcId] = {
47
+ ...declared,
31
48
  pikkuFuncId: funcId,
32
- inputSchemaName: null,
33
- outputSchemaName: null,
34
- sessionless: true,
49
+ inputSchemaName: declared?.inputSchemaName ?? null,
50
+ outputSchemaName: declared?.outputSchemaName ?? null,
51
+ sessionless: declared?.sessionless ?? true,
35
52
  }
36
53
  addFunction(funcId, config.func as any)
37
54
  return funcId
38
55
  }
39
56
 
57
+ /**
58
+ * Tag middleware the inspector resolved for this gateway. It is keyed by
59
+ * gateway name rather than reachable from `config`, because `tags` is a
60
+ * compile-time input everywhere — nothing at runtime maps a tag to its
61
+ * middleware group.
62
+ */
63
+ const gatewayInheritedMiddleware = (config: CoreGateway) =>
64
+ pikkuState(null, 'gateway', 'meta')[config.name]?.middleware
65
+
40
66
  export const resolveGatewayAdapter = (
41
67
  config: CoreGateway,
42
68
  services: CoreSingletonServices
@@ -134,6 +160,7 @@ const wireWebhookGateway = (config: CoreGateway): void => {
134
160
  const createWebhookPostHandler = (config: CoreGateway) => {
135
161
  const { name, middleware: userMiddleware } = config
136
162
  const handlerFuncId = registerGatewayHandler(config)
163
+ const inheritedMiddleware = gatewayInheritedMiddleware(config)
137
164
 
138
165
  return async (
139
166
  services: CoreSingletonServices,
@@ -169,6 +196,7 @@ const createWebhookPostHandler = (config: CoreGateway) => {
169
196
  singletonServices: services,
170
197
  data: () => parsed,
171
198
  auth: config.auth,
199
+ inheritedMiddleware,
172
200
  wire: wire as any,
173
201
  })
174
202
  }
@@ -253,6 +281,7 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
253
281
 
254
282
  const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
255
283
  const handlerFuncId = registerGatewayHandler(config)
284
+ const inheritedMiddleware = gatewayInheritedMiddleware(config)
256
285
 
257
286
  addFunction(connectFuncId, {
258
287
  auth: false,
@@ -292,6 +321,7 @@ const wireWebsocketGateway = (config: CoreGateway): void => {
292
321
  singletonServices: services,
293
322
  data: () => parsed,
294
323
  auth: config.auth,
324
+ inheritedMiddleware,
295
325
  wire: wire as any,
296
326
  })
297
327
  }
@@ -333,6 +363,7 @@ export const createListenerMessageHandler = (
333
363
  ): ((rawData: unknown) => Promise<void>) => {
334
364
  const userMiddleware = config.middleware as CorePikkuMiddleware[] | undefined
335
365
  const handlerFuncId = registerGatewayHandler(config)
366
+ const inheritedMiddleware = gatewayInheritedMiddleware(config)
336
367
 
337
368
  return async (rawData: unknown): Promise<void> => {
338
369
  const adapter = await resolveGatewayAdapter(config, singletonServices)
@@ -354,6 +385,7 @@ export const createListenerMessageHandler = (
354
385
  singletonServices,
355
386
  data: () => parsed,
356
387
  auth: config.auth,
388
+ inheritedMiddleware,
357
389
  wire,
358
390
  })
359
391
  }
@@ -108,6 +108,53 @@ describe('validateAndBuildSecretDefinitionsMeta', () => {
108
108
  )
109
109
  })
110
110
 
111
+ test('should carry allowedHosts into the meta', () => {
112
+ const definitions = [
113
+ {
114
+ name: 'example-api',
115
+ displayName: 'Example API',
116
+ secretId: 'EXAMPLE_API_CREDENTIALS',
117
+ allowedHosts: ['api.example.com', '*.example.com'],
118
+ sourceFile: 'a.ts',
119
+ },
120
+ ]
121
+ const result = validateAndBuildSecretDefinitionsMeta(
122
+ definitions as any,
123
+ new Map()
124
+ )
125
+ assert.deepStrictEqual(result['example-api']!.allowedHosts, [
126
+ 'api.example.com',
127
+ '*.example.com',
128
+ ])
129
+ })
130
+
131
+ test('should carry allowedHosts into the meta for a shared secretId', () => {
132
+ const definitions = [
133
+ {
134
+ name: 'cred1',
135
+ displayName: 'Cred 1',
136
+ secretId: 'SHARED',
137
+ allowedHosts: ['first.example.com'],
138
+ sourceFile: 'a.ts',
139
+ },
140
+ {
141
+ name: 'cred2',
142
+ displayName: 'Cred 2',
143
+ secretId: 'SHARED',
144
+ allowedHosts: ['second.example.com'],
145
+ sourceFile: 'b.ts',
146
+ },
147
+ ]
148
+ const result = validateAndBuildSecretDefinitionsMeta(
149
+ definitions as any,
150
+ new Map()
151
+ )
152
+ assert.deepStrictEqual(result['cred1']!.allowedHosts, ['first.example.com'])
153
+ assert.deepStrictEqual(result['cred2']!.allowedHosts, [
154
+ 'second.example.com',
155
+ ])
156
+ })
157
+
111
158
  test('should handle empty definitions', () => {
112
159
  const result = validateAndBuildSecretDefinitionsMeta([], new Map())
113
160
  assert.deepStrictEqual(result, {})
@@ -57,6 +57,7 @@ export function validateAndBuildSecretDefinitionsMeta(
57
57
  oauth2: def.oauth2,
58
58
  rotationPeriod: def.rotationPeriod,
59
59
  docsUrl: def.docsUrl,
60
+ allowedHosts: def.allowedHosts,
60
61
  sourceFile: def.sourceFile,
61
62
  }
62
63
  }
@@ -75,6 +76,7 @@ export function validateAndBuildSecretDefinitionsMeta(
75
76
  oauth2: def.oauth2,
76
77
  rotationPeriod: def.rotationPeriod,
77
78
  docsUrl: def.docsUrl,
79
+ allowedHosts: def.allowedHosts,
78
80
  sourceFile: def.sourceFile,
79
81
  }
80
82
  }