@platformatic/runtime 3.11.0 → 3.13.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.
- package/config.d.ts +9 -0
- package/index.js +1 -1
- package/lib/config.js +10 -2
- package/lib/management-api.js +28 -0
- package/lib/policies.js +23 -0
- package/lib/runtime.js +35 -18
- package/lib/worker/controller.js +14 -2
- package/lib/worker/interceptors.js +2 -0
- package/lib/worker/messaging.js +9 -3
- package/package.json +16 -16
- package/schema.json +29 -1
package/config.d.ts
CHANGED
|
@@ -413,4 +413,13 @@ export type PlatformaticRuntimeConfig = {
|
|
|
413
413
|
maxRetries?: number;
|
|
414
414
|
[k: string]: unknown;
|
|
415
415
|
}[];
|
|
416
|
+
policies?: {
|
|
417
|
+
deny: {
|
|
418
|
+
/**
|
|
419
|
+
* This interface was referenced by `undefined`'s JSON-Schema definition
|
|
420
|
+
* via the `patternProperty` "^.*$".
|
|
421
|
+
*/
|
|
422
|
+
[k: string]: string | [string, ...string[]];
|
|
423
|
+
};
|
|
424
|
+
};
|
|
416
425
|
};
|
package/index.js
CHANGED
|
@@ -44,7 +44,7 @@ function handleSignal (runtime, config) {
|
|
|
44
44
|
|
|
45
45
|
const cwg = closeWithGrace({ delay: config.gracefulShutdown?.runtime ?? 10000, onTimeout }, async event => {
|
|
46
46
|
if (event.err instanceof Error) {
|
|
47
|
-
console.error(event.err)
|
|
47
|
+
console.error(new Error('@platformatic/runtime threw an unexpected error', { cause: event.err }))
|
|
48
48
|
}
|
|
49
49
|
await runtime.close()
|
|
50
50
|
})
|
package/lib/config.js
CHANGED
|
@@ -335,13 +335,21 @@ export async function transform (config, _, context) {
|
|
|
335
335
|
// like adding other applications.
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
if (config.metrics ===
|
|
338
|
+
if (typeof config.metrics === 'boolean') {
|
|
339
339
|
config.metrics = {
|
|
340
|
-
enabled:
|
|
340
|
+
enabled: config.metrics,
|
|
341
341
|
timeout: 1000
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
if (config.policies?.deny) {
|
|
346
|
+
for (const [from, to] of Object.entries(config.policies.deny)) {
|
|
347
|
+
if (typeof to === 'string') {
|
|
348
|
+
config.policies.deny[from] = [to]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
345
353
|
config.applications = applications
|
|
346
354
|
config.web = undefined
|
|
347
355
|
config.services = undefined
|
package/lib/management-api.js
CHANGED
|
@@ -132,6 +132,17 @@ export async function managementApiPlugin (app, opts) {
|
|
|
132
132
|
})
|
|
133
133
|
|
|
134
134
|
app.get('/metrics', { logLevel: 'debug' }, async (req, reply) => {
|
|
135
|
+
const config = await runtime.getRuntimeConfig()
|
|
136
|
+
|
|
137
|
+
if (config.metrics?.enabled === false) {
|
|
138
|
+
reply.code(501)
|
|
139
|
+
return {
|
|
140
|
+
statusCode: 501,
|
|
141
|
+
error: 'Not Implemented',
|
|
142
|
+
message: 'Metrics are disabled.'
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
135
146
|
const accepts = req.accepts()
|
|
136
147
|
|
|
137
148
|
if (!accepts.type('text/plain') && accepts.type('application/json')) {
|
|
@@ -145,6 +156,23 @@ export async function managementApiPlugin (app, opts) {
|
|
|
145
156
|
})
|
|
146
157
|
|
|
147
158
|
app.get('/metrics/live', { websocket: true }, async socket => {
|
|
159
|
+
const config = await runtime.getRuntimeConfig()
|
|
160
|
+
|
|
161
|
+
if (config.metrics?.enabled === false) {
|
|
162
|
+
socket.send(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
statusCode: 501,
|
|
165
|
+
error: 'Not Implemented',
|
|
166
|
+
message: 'Metrics are disabled.'
|
|
167
|
+
}),
|
|
168
|
+
() => {
|
|
169
|
+
socket.close()
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
const cachedMetrics = runtime.getCachedMetrics()
|
|
149
177
|
if (cachedMetrics.length > 0) {
|
|
150
178
|
const serializedMetrics = cachedMetrics.map(metric => JSON.stringify(metric)).join('\n')
|
package/lib/policies.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createChannelCreationHook (config) {
|
|
2
|
+
const denyList = config.policies?.deny
|
|
3
|
+
|
|
4
|
+
if (typeof denyList === 'undefined') {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const forbidden = new Set()
|
|
9
|
+
|
|
10
|
+
for (let [first, unalloweds] of Object.entries(denyList)) {
|
|
11
|
+
for (let second of unalloweds) {
|
|
12
|
+
first = first.toLowerCase()
|
|
13
|
+
second = second.toLowerCase()
|
|
14
|
+
|
|
15
|
+
forbidden.add(`${first}:${second}`)
|
|
16
|
+
forbidden.add(`${second}:${first}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return function channelCreationHook (first, second) {
|
|
21
|
+
return !forbidden.has(`${first.toLowerCase()}:${second.toLowerCase()}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/runtime.js
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from './errors.js'
|
|
41
41
|
import { abstractLogger, createLogger } from './logger.js'
|
|
42
42
|
import { startManagementApi } from './management-api.js'
|
|
43
|
+
import { getMemoryInfo } from './metrics.js'
|
|
44
|
+
import { createChannelCreationHook } from './policies.js'
|
|
43
45
|
import { startPrometheusServer } from './prom-server.js'
|
|
44
46
|
import ScalingAlgorithm from './scaling-algorithm.js'
|
|
45
47
|
import { startScheduler } from './scheduler.js'
|
|
@@ -47,7 +49,6 @@ import { createSharedStore } from './shared-http-cache.js'
|
|
|
47
49
|
import { version } from './version.js'
|
|
48
50
|
import { sendViaITC, waitEventFromITC } from './worker/itc.js'
|
|
49
51
|
import { RoundRobinMap } from './worker/round-robin-map.js'
|
|
50
|
-
import { getMemoryInfo } from './metrics.js'
|
|
51
52
|
import {
|
|
52
53
|
kApplicationId,
|
|
53
54
|
kConfig,
|
|
@@ -59,8 +60,8 @@ import {
|
|
|
59
60
|
kStderrMarker,
|
|
60
61
|
kWorkerId,
|
|
61
62
|
kWorkersBroadcast,
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
kWorkerStartTime,
|
|
64
|
+
kWorkerStatus
|
|
64
65
|
} from './worker/symbols.js'
|
|
65
66
|
|
|
66
67
|
const kWorkerFile = join(import.meta.dirname, 'worker/main.js')
|
|
@@ -113,6 +114,8 @@ export class Runtime extends EventEmitter {
|
|
|
113
114
|
#sharedHttpCache
|
|
114
115
|
#scheduler
|
|
115
116
|
|
|
117
|
+
#channelCreationHook
|
|
118
|
+
|
|
116
119
|
constructor (config, context) {
|
|
117
120
|
super()
|
|
118
121
|
this.setMaxListeners(MAX_LISTENERS_COUNT)
|
|
@@ -125,7 +128,12 @@ export class Runtime extends EventEmitter {
|
|
|
125
128
|
this.#concurrency = this.#context.concurrency ?? MAX_CONCURRENCY
|
|
126
129
|
this.#workers = new RoundRobinMap()
|
|
127
130
|
this.#url = undefined
|
|
128
|
-
this.#
|
|
131
|
+
this.#channelCreationHook = createChannelCreationHook(this.#config)
|
|
132
|
+
this.#meshInterceptor = createThreadInterceptor({
|
|
133
|
+
domain: '.plt.local',
|
|
134
|
+
timeout: this.#config.applicationTimeout,
|
|
135
|
+
onChannelCreation: this.#channelCreationHook
|
|
136
|
+
})
|
|
129
137
|
this.logger = abstractLogger // This is replaced by the real logger in init() and eventually removed in close()
|
|
130
138
|
this.#status = undefined
|
|
131
139
|
this.#restartingWorkers = new Map()
|
|
@@ -275,7 +283,7 @@ export class Runtime extends EventEmitter {
|
|
|
275
283
|
|
|
276
284
|
this.#updateStatus('started')
|
|
277
285
|
|
|
278
|
-
if (this.#
|
|
286
|
+
if (this.#config.metrics?.enabled !== false && typeof this.#metrics === 'undefined') {
|
|
279
287
|
this.startCollectingMetrics()
|
|
280
288
|
}
|
|
281
289
|
|
|
@@ -1606,7 +1614,7 @@ export class Runtime extends EventEmitter {
|
|
|
1606
1614
|
} else {
|
|
1607
1615
|
worker[kHealthCheckTimer].refresh()
|
|
1608
1616
|
}
|
|
1609
|
-
}, interval)
|
|
1617
|
+
}, interval).unref()
|
|
1610
1618
|
}
|
|
1611
1619
|
|
|
1612
1620
|
async #startWorker (
|
|
@@ -2003,7 +2011,14 @@ export class Runtime extends EventEmitter {
|
|
|
2003
2011
|
}
|
|
2004
2012
|
}
|
|
2005
2013
|
|
|
2006
|
-
async #getWorkerMessagingChannel ({ application, worker }, context) {
|
|
2014
|
+
async #getWorkerMessagingChannel ({ id, application, worker }, context) {
|
|
2015
|
+
if (this.#channelCreationHook?.(id, application) === false) {
|
|
2016
|
+
throw new MessagingError(
|
|
2017
|
+
application,
|
|
2018
|
+
`Communication channels are disabled between applications "${id}" and "${application}".`
|
|
2019
|
+
)
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2007
2022
|
const target = await this.#getWorkerById(application, worker, true, true)
|
|
2008
2023
|
|
|
2009
2024
|
const { port1, port2 } = new MessageChannel()
|
|
@@ -2085,8 +2100,15 @@ export class Runtime extends EventEmitter {
|
|
|
2085
2100
|
}
|
|
2086
2101
|
}
|
|
2087
2102
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2103
|
+
let pinoLog
|
|
2104
|
+
|
|
2105
|
+
if (typeof message === 'object') {
|
|
2106
|
+
pinoLog =
|
|
2107
|
+
typeof message.level === 'number' &&
|
|
2108
|
+
// We want to accept both pino raw time (number) and time as formatted string
|
|
2109
|
+
(typeof message.time === 'number' || typeof message.time === 'string') &&
|
|
2110
|
+
typeof message.msg === 'string'
|
|
2111
|
+
}
|
|
2090
2112
|
|
|
2091
2113
|
// Directly write to the Pino destination
|
|
2092
2114
|
if (pinoLog) {
|
|
@@ -2467,9 +2489,7 @@ export class Runtime extends EventEmitter {
|
|
|
2467
2489
|
async #setupVerticalScaler () {
|
|
2468
2490
|
const fixedWorkersCount = this.#config.workers
|
|
2469
2491
|
if (fixedWorkersCount !== undefined) {
|
|
2470
|
-
this.logger.warn(
|
|
2471
|
-
`Vertical scaler disabled because the "workers" configuration is set to ${fixedWorkersCount}`
|
|
2472
|
-
)
|
|
2492
|
+
this.logger.warn(`Vertical scaler disabled because the "workers" configuration is set to ${fixedWorkersCount}`)
|
|
2473
2493
|
return
|
|
2474
2494
|
}
|
|
2475
2495
|
|
|
@@ -2510,7 +2530,7 @@ export class Runtime extends EventEmitter {
|
|
|
2510
2530
|
if (application.entrypoint && !features.node.reusePort) {
|
|
2511
2531
|
this.logger.warn(
|
|
2512
2532
|
`The "${application.id}" application cannot be scaled because it is an entrypoint` +
|
|
2513
|
-
|
|
2533
|
+
' and the "reusePort" feature is not available in your OS.'
|
|
2514
2534
|
)
|
|
2515
2535
|
|
|
2516
2536
|
applicationsConfigs[application.id] = {
|
|
@@ -2522,7 +2542,7 @@ export class Runtime extends EventEmitter {
|
|
|
2522
2542
|
if (application.workers !== undefined) {
|
|
2523
2543
|
this.logger.warn(
|
|
2524
2544
|
`The "${application.id}" application cannot be scaled because` +
|
|
2525
|
-
|
|
2545
|
+
` it has a fixed number of workers (${application.workers}).`
|
|
2526
2546
|
)
|
|
2527
2547
|
applicationsConfigs[application.id] = {
|
|
2528
2548
|
minWorkers: application.workers,
|
|
@@ -2574,10 +2594,7 @@ export class Runtime extends EventEmitter {
|
|
|
2574
2594
|
const now = Date.now()
|
|
2575
2595
|
|
|
2576
2596
|
for (const worker of this.#workers.values()) {
|
|
2577
|
-
if (
|
|
2578
|
-
worker[kWorkerStatus] !== 'started' ||
|
|
2579
|
-
worker[kWorkerStartTime] + gracePeriod > now
|
|
2580
|
-
) {
|
|
2597
|
+
if (worker[kWorkerStatus] !== 'started' || worker[kWorkerStartTime] + gracePeriod > now) {
|
|
2581
2598
|
continue
|
|
2582
2599
|
}
|
|
2583
2600
|
|
package/lib/worker/controller.js
CHANGED
|
@@ -160,12 +160,13 @@ export class Controller extends EventEmitter {
|
|
|
160
160
|
this.#logAndThrow(err)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
this.emit('starting')
|
|
164
|
-
|
|
165
163
|
if (this.capability.status === 'stopped') {
|
|
166
164
|
return
|
|
167
165
|
}
|
|
168
166
|
|
|
167
|
+
this.capability.updateStatus('starting')
|
|
168
|
+
this.emit('starting')
|
|
169
|
+
|
|
169
170
|
if (this.#watch) {
|
|
170
171
|
const watchConfig = await this.capability.getWatchConfig()
|
|
171
172
|
|
|
@@ -187,6 +188,9 @@ export class Controller extends EventEmitter {
|
|
|
187
188
|
this.#listening = listen
|
|
188
189
|
/* c8 ignore next 5 */
|
|
189
190
|
} catch (err) {
|
|
191
|
+
this.capability.updateStatus('start:error')
|
|
192
|
+
this.emit('start:error', err)
|
|
193
|
+
|
|
190
194
|
this.capability.log({ message: err.message, level: 'debug' })
|
|
191
195
|
this.#starting = false
|
|
192
196
|
throw err
|
|
@@ -194,6 +198,8 @@ export class Controller extends EventEmitter {
|
|
|
194
198
|
|
|
195
199
|
this.#started = true
|
|
196
200
|
this.#starting = false
|
|
201
|
+
|
|
202
|
+
this.capability.updateStatus('started')
|
|
197
203
|
this.emit('started')
|
|
198
204
|
}
|
|
199
205
|
|
|
@@ -203,6 +209,10 @@ export class Controller extends EventEmitter {
|
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
this.emit('stopping')
|
|
212
|
+
// Do not update status of the capability to "stopping" here otherwise
|
|
213
|
+
// if stop is called before start is finished, the capability will not
|
|
214
|
+
// be able to wait for start to finish and it will create a race condition.
|
|
215
|
+
|
|
206
216
|
await this.#stopFileWatching()
|
|
207
217
|
await this.capability.waitForDependentsStop(dependents)
|
|
208
218
|
await this.capability.stop()
|
|
@@ -210,6 +220,8 @@ export class Controller extends EventEmitter {
|
|
|
210
220
|
this.#started = false
|
|
211
221
|
this.#starting = false
|
|
212
222
|
this.#listening = false
|
|
223
|
+
|
|
224
|
+
this.capability.updateStatus('stopped')
|
|
213
225
|
this.emit('stopped')
|
|
214
226
|
}
|
|
215
227
|
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from 'node:url'
|
|
|
5
5
|
import { parentPort, workerData } from 'node:worker_threads'
|
|
6
6
|
import { Agent, Client, Pool, setGlobalDispatcher } from 'undici'
|
|
7
7
|
import { wire } from 'undici-thread-interceptor'
|
|
8
|
+
import { createChannelCreationHook } from '../policies.js'
|
|
8
9
|
import { RemoteCacheStore, httpCacheInterceptor } from './http-cache.js'
|
|
9
10
|
import { kInterceptors } from './symbols.js'
|
|
10
11
|
|
|
@@ -171,6 +172,7 @@ function createThreadInterceptor (runtimeConfig) {
|
|
|
171
172
|
domain: '.plt.local',
|
|
172
173
|
port: parentPort,
|
|
173
174
|
timeout: runtimeConfig.applicationTimeout,
|
|
175
|
+
onChannelCreation: createChannelCreationHook(runtimeConfig),
|
|
174
176
|
...telemetryHooks
|
|
175
177
|
})
|
|
176
178
|
return threadDispatcher
|
package/lib/worker/messaging.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ensureLoggableError, executeWithTimeout, kTimeout } from '@platformatic/foundation'
|
|
2
|
+
import { errors, generateRequest, generateResponse, ITC, parseRequest, sanitize } from '@platformatic/itc'
|
|
3
3
|
import { MessagingError } from '../errors.js'
|
|
4
4
|
import { RoundRobinMap } from './round-robin-map.js'
|
|
5
5
|
import { kITC, kWorkersBroadcast } from './symbols.js'
|
|
@@ -7,6 +7,7 @@ import { kITC, kWorkersBroadcast } from './symbols.js'
|
|
|
7
7
|
const kPendingResponses = Symbol('plt.messaging.pendingResponses')
|
|
8
8
|
|
|
9
9
|
export class MessagingITC extends ITC {
|
|
10
|
+
#id
|
|
10
11
|
#timeout
|
|
11
12
|
#listener
|
|
12
13
|
#closeResolvers
|
|
@@ -22,6 +23,7 @@ export class MessagingITC extends ITC {
|
|
|
22
23
|
name: `${id}-messaging`
|
|
23
24
|
})
|
|
24
25
|
|
|
26
|
+
this.#id = id
|
|
25
27
|
this.#timeout = runtimeConfig.messagingTimeout
|
|
26
28
|
this.#workers = new RoundRobinMap()
|
|
27
29
|
this.#sources = new Set()
|
|
@@ -67,7 +69,11 @@ export class MessagingITC extends ITC {
|
|
|
67
69
|
// Use twice the value here as a fallback measure. The target handler in the main thread is forwarding
|
|
68
70
|
// the request to the worker, using executeWithTimeout with the user set timeout value.
|
|
69
71
|
const channel = await executeWithTimeout(
|
|
70
|
-
globalThis[kITC].send('getWorkerMessagingChannel', {
|
|
72
|
+
globalThis[kITC].send('getWorkerMessagingChannel', {
|
|
73
|
+
id: this.#id,
|
|
74
|
+
application: worker.application,
|
|
75
|
+
worker: worker.worker
|
|
76
|
+
}),
|
|
71
77
|
this.#timeout * 2
|
|
72
78
|
)
|
|
73
79
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"typescript": "^5.5.4",
|
|
36
36
|
"undici-oidc-interceptor": "^0.5.0",
|
|
37
37
|
"why-is-node-running": "^2.2.2",
|
|
38
|
-
"@platformatic/
|
|
39
|
-
"@platformatic/db": "3.
|
|
40
|
-
"@platformatic/
|
|
41
|
-
"@platformatic/
|
|
42
|
-
"@platformatic/service": "3.
|
|
43
|
-
"@platformatic/sql-
|
|
44
|
-
"@platformatic/
|
|
45
|
-
"@platformatic/
|
|
38
|
+
"@platformatic/gateway": "3.13.0",
|
|
39
|
+
"@platformatic/db": "3.13.0",
|
|
40
|
+
"@platformatic/node": "3.13.0",
|
|
41
|
+
"@platformatic/composer": "3.13.0",
|
|
42
|
+
"@platformatic/service": "3.13.0",
|
|
43
|
+
"@platformatic/sql-graphql": "3.13.0",
|
|
44
|
+
"@platformatic/sql-mapper": "3.13.0",
|
|
45
|
+
"@platformatic/wattpm-pprof-capture": "3.13.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@fastify/accepts": "^5.0.0",
|
|
@@ -71,14 +71,14 @@
|
|
|
71
71
|
"sonic-boom": "^4.2.0",
|
|
72
72
|
"systeminformation": "^5.27.11",
|
|
73
73
|
"undici": "^7.0.0",
|
|
74
|
-
"undici-thread-interceptor": "^0.
|
|
74
|
+
"undici-thread-interceptor": "^0.15.0",
|
|
75
75
|
"ws": "^8.16.0",
|
|
76
|
-
"@platformatic/basic": "3.
|
|
77
|
-
"@platformatic/foundation": "3.
|
|
78
|
-
"@platformatic/
|
|
79
|
-
"@platformatic/
|
|
80
|
-
"@platformatic/metrics": "3.
|
|
81
|
-
"@platformatic/telemetry": "3.
|
|
76
|
+
"@platformatic/basic": "3.13.0",
|
|
77
|
+
"@platformatic/foundation": "3.13.0",
|
|
78
|
+
"@platformatic/generators": "3.13.0",
|
|
79
|
+
"@platformatic/itc": "3.13.0",
|
|
80
|
+
"@platformatic/metrics": "3.13.0",
|
|
81
|
+
"@platformatic/telemetry": "3.13.0"
|
|
82
82
|
},
|
|
83
83
|
"engines": {
|
|
84
84
|
"node": ">=22.19.0"
|
package/schema.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.
|
|
2
|
+
"$id": "https://schemas.platformatic.dev/@platformatic/runtime/3.13.0.json",
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Platformatic Runtime Config",
|
|
5
5
|
"type": "object",
|
|
@@ -2183,6 +2183,34 @@
|
|
|
2183
2183
|
"callbackUrl"
|
|
2184
2184
|
]
|
|
2185
2185
|
}
|
|
2186
|
+
},
|
|
2187
|
+
"policies": {
|
|
2188
|
+
"type": "object",
|
|
2189
|
+
"properties": {
|
|
2190
|
+
"deny": {
|
|
2191
|
+
"type": "object",
|
|
2192
|
+
"patternProperties": {
|
|
2193
|
+
"^.*$": {
|
|
2194
|
+
"oneOf": [
|
|
2195
|
+
{
|
|
2196
|
+
"type": "string"
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
"type": "array",
|
|
2200
|
+
"items": {
|
|
2201
|
+
"type": "string"
|
|
2202
|
+
},
|
|
2203
|
+
"minItems": 1
|
|
2204
|
+
}
|
|
2205
|
+
]
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
},
|
|
2210
|
+
"required": [
|
|
2211
|
+
"deny"
|
|
2212
|
+
],
|
|
2213
|
+
"additionalProperties": false
|
|
2186
2214
|
}
|
|
2187
2215
|
},
|
|
2188
2216
|
"anyOf": [
|