@toa.io/extensions.configuration 1.0.0-alpha.284 → 1.0.0-alpha.287
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 +1 -365
- package/components/configuration.values/manifest.toa.yaml +14 -21
- package/components/configuration.values/migrations/0001-system-properties.yaml +14 -0
- package/components/configuration.values/migrations/0002-indexes.yaml +7 -0
- package/components/configuration.values/operations/create.js +1 -2
- package/components/configuration.values/operations/fetch.js +1 -1
- package/components/configuration.values/operations/get.js +1 -1
- package/components/configuration.values/operations/lib/map.js +1 -1
- package/components/configuration.values/operations/lib/resolve.js +3 -3
- package/components/configuration.values/operations/list.js +1 -1
- package/components/configuration.values/source/create.ts +3 -5
- package/components/configuration.values/source/fetch.ts +12 -10
- package/components/configuration.values/source/get.ts +2 -3
- package/components/configuration.values/source/lib/map.test.ts +7 -2
- package/components/configuration.values/source/lib/map.ts +3 -3
- package/components/configuration.values/source/lib/resolve.ts +9 -6
- package/components/configuration.values/source/list.ts +8 -6
- package/components/configuration.values/tsconfig.tsbuildinfo +1 -1
- package/components/configuration.values/types/toa.d.ts +5 -5
- package/cucumber.mjs +3 -0
- package/features/site/index.html +6 -2
- package/features/steps/UI.ts +20 -13
- package/notes/consistency.md +10 -9
- package/package.json +6 -6
- package/readme.md +15 -14
- package/schemas/annotation.cos.yaml +1 -1
- package/source/Aspect.test.ts +32 -8
- package/source/Aspect.ts +28 -19
- package/source/Client.test.ts +31 -14
- package/source/Client.ts +41 -48
- package/source/Composition.ts +6 -6
- package/source/Factory.ts +7 -7
- package/source/Secret.test.ts +1 -1
- package/source/Secret.ts +5 -5
- package/source/UI.ts +33 -32
- package/source/configuration.test.ts +14 -8
- package/source/configuration.ts +11 -15
- package/source/const.ts +13 -1
- package/source/context.ts +2 -2
- package/source/deployment.test.ts +37 -12
- package/source/deployment.ts +45 -19
- package/source/epoch.test.ts +8 -2
- package/source/epoch.ts +6 -5
- package/source/index.ts +1 -1
- package/source/manifest.test.ts +12 -6
- package/source/manifest.ts +1 -1
- package/transpiled/Aspect.d.ts +2 -1
- package/transpiled/Aspect.js +15 -3
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Client.d.ts +1 -1
- package/transpiled/Client.js +8 -2
- package/transpiled/Client.js.map +1 -1
- package/transpiled/Composition.js +2 -2
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +2 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Secret.js.map +1 -1
- package/transpiled/UI.js +2 -1
- package/transpiled/UI.js.map +1 -1
- package/transpiled/configuration.js.map +1 -1
- package/transpiled/const.d.ts +8 -1
- package/transpiled/const.js +10 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/context.d.ts +1 -1
- package/transpiled/context.js.map +1 -1
- package/transpiled/deployment.d.ts +3 -0
- package/transpiled/deployment.js +19 -5
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/epoch.js +3 -1
- package/transpiled/epoch.js.map +1 -1
- package/transpiled/index.d.ts +1 -1
- package/transpiled/index.js +1 -1
- package/transpiled/index.js.map +1 -1
- package/transpiled/manifest.js.map +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/immutable/chunks/{BzG-gka8.js → BW57cOwz.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{_I_MUoRn.js → Bhnl1Avs.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{Bxde3uE_.js → BxOvOexd.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{mvspXr3J.js → CkX_FNYa.js} +2 -2
- package/ui/dist/_app/immutable/chunks/{p9RyiXQ-.js → Dmv4yvb8.js} +1 -1
- package/ui/dist/_app/immutable/entry/{app.C1VIepN7.js → app.Cr7od9uH.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.DWf-Tl0X.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.D_0dtFej.js → 0.CcGZUFKo.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.DFlgHh-H.js → 1.CR6En12y.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.BIHgAxXR.js → 2.BhyO8bGT.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.DcbMtJtb.js +1 -0
- package/ui/dist/_app/immutable/nodes/{4.BZMuJGfA.js → 4.qVnVQBNq.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +28 -23
- package/ui/dist/_app/immutable/entry/start.BQWCs_Pq.js +0 -1
- package/ui/dist/_app/immutable/nodes/3.R-_yCt5X.js +0 -1
package/source/Client.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { console } from 'openspan'
|
|
2
2
|
import { Connector, Locator } from '@toa.io/core'
|
|
3
3
|
import { EVENT, SOURCE } from './const.js'
|
|
4
|
-
import type {
|
|
4
|
+
import type { Remote } from '@toa.io/core'
|
|
5
|
+
import type { Message } from '@toa.io/core/types'
|
|
5
6
|
import type { Host } from './Factory.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -23,7 +24,7 @@ export class Client extends Connector {
|
|
|
23
24
|
private fresh = false
|
|
24
25
|
private round = 0
|
|
25
26
|
|
|
26
|
-
public constructor
|
|
27
|
+
public constructor(host: Host, options: Partial<Options> = {}) {
|
|
27
28
|
super()
|
|
28
29
|
|
|
29
30
|
this.host = host
|
|
@@ -31,7 +32,7 @@ export class Client extends Connector {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/** The configuration of a component for an epoch, once the service has one. */
|
|
34
|
-
public async fetch
|
|
35
|
+
public async fetch(component: string, epoch: string): Promise<Value> {
|
|
35
36
|
const key = id(component, epoch)
|
|
36
37
|
|
|
37
38
|
let entry = this.pending.get(key)
|
|
@@ -45,28 +46,25 @@ export class Client extends Connector {
|
|
|
45
46
|
entry!.waiters.push(resolve)
|
|
46
47
|
})
|
|
47
48
|
|
|
48
|
-
if (this.flushing)
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
this.schedule(0)
|
|
49
|
+
if (this.flushing) this.fresh = true
|
|
50
|
+
else this.schedule(0)
|
|
52
51
|
|
|
53
52
|
return await promise
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
public subscribe
|
|
55
|
+
public subscribe(component: string, epoch: string, listener: Listener): void {
|
|
57
56
|
const key = id(component, epoch)
|
|
58
57
|
|
|
59
|
-
if (!this.listeners.has(key))
|
|
60
|
-
this.listeners.set(key, new Set())
|
|
58
|
+
if (!this.listeners.has(key)) this.listeners.set(key, new Set())
|
|
61
59
|
|
|
62
60
|
this.listeners.get(key)!.add(listener)
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
public unsubscribe
|
|
63
|
+
public unsubscribe(component: string, epoch: string, listener: Listener): void {
|
|
66
64
|
this.listeners.get(id(component, epoch))?.delete(listener)
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
protected override async open
|
|
67
|
+
protected override async open(): Promise<void> {
|
|
70
68
|
this.remote = await this.host.remote(LOCATOR, SOURCE)
|
|
71
69
|
|
|
72
70
|
this.depends(this.remote)
|
|
@@ -79,25 +77,23 @@ export class Client extends Connector {
|
|
|
79
77
|
await consumer.connect()
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
protected override async close
|
|
80
|
+
protected override async close(): Promise<void> {
|
|
83
81
|
if (this.timer !== null) {
|
|
84
82
|
clearTimeout(this.timer)
|
|
85
83
|
this.timer = null
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
protected override async dispose
|
|
87
|
+
protected override async dispose(): Promise<void> {
|
|
90
88
|
this.disposed = true
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
private schedule
|
|
94
|
-
if (this.flushing)
|
|
95
|
-
return
|
|
91
|
+
private schedule(delay: number): void {
|
|
92
|
+
if (this.flushing) return
|
|
96
93
|
|
|
97
94
|
if (this.timer !== null) {
|
|
98
95
|
// a request that has just arrived does not wait for the round already planned
|
|
99
|
-
if (delay > 0)
|
|
100
|
-
return
|
|
96
|
+
if (delay > 0) return
|
|
101
97
|
|
|
102
98
|
clearTimeout(this.timer)
|
|
103
99
|
}
|
|
@@ -107,7 +103,7 @@ export class Client extends Connector {
|
|
|
107
103
|
}, delay)
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
private async flush
|
|
106
|
+
private async flush(): Promise<void> {
|
|
111
107
|
this.timer = null
|
|
112
108
|
this.flushing = true
|
|
113
109
|
this.fresh = false
|
|
@@ -118,8 +114,7 @@ export class Client extends Connector {
|
|
|
118
114
|
try {
|
|
119
115
|
const output = await this.remote!.invoke<Fetched[] | Error>('fetch', { input })
|
|
120
116
|
|
|
121
|
-
if (output instanceof Error)
|
|
122
|
-
throw output
|
|
117
|
+
if (output instanceof Error) throw output
|
|
123
118
|
|
|
124
119
|
this.settle(output)
|
|
125
120
|
} catch (error) {
|
|
@@ -130,35 +125,29 @@ export class Client extends Connector {
|
|
|
130
125
|
|
|
131
126
|
this.report(batch)
|
|
132
127
|
|
|
133
|
-
if (this.pending.size === 0)
|
|
134
|
-
return
|
|
128
|
+
if (this.pending.size === 0) return
|
|
135
129
|
|
|
136
|
-
if (this.fresh)
|
|
137
|
-
|
|
138
|
-
else
|
|
139
|
-
this.schedule(this.backoff())
|
|
130
|
+
if (this.fresh) this.schedule(0)
|
|
131
|
+
else this.schedule(this.backoff())
|
|
140
132
|
}
|
|
141
133
|
|
|
142
134
|
/** Those the service has served are told; the rest stay for the next round. */
|
|
143
|
-
private settle
|
|
135
|
+
private settle(output: Fetched[]): void {
|
|
144
136
|
for (const { component, epoch, configuration, created } of output) {
|
|
145
|
-
if (configuration === null)
|
|
146
|
-
continue
|
|
137
|
+
if (configuration === null) continue
|
|
147
138
|
|
|
148
139
|
const key = id(component, epoch)
|
|
149
140
|
const entry = this.pending.get(key)
|
|
150
141
|
|
|
151
|
-
if (entry === undefined)
|
|
152
|
-
continue
|
|
142
|
+
if (entry === undefined) continue
|
|
153
143
|
|
|
154
144
|
this.pending.delete(key)
|
|
155
145
|
|
|
156
|
-
for (const resolve of entry.waiters)
|
|
157
|
-
resolve({ configuration, created })
|
|
146
|
+
for (const resolve of entry.waiters) resolve({ configuration, created })
|
|
158
147
|
}
|
|
159
148
|
}
|
|
160
149
|
|
|
161
|
-
private report
|
|
150
|
+
private report(batch: Pending[]): void {
|
|
162
151
|
const waiting = batch
|
|
163
152
|
.filter(({ component, epoch }) => this.pending.has(id(component, epoch)))
|
|
164
153
|
.map(({ component }) => component)
|
|
@@ -172,24 +161,28 @@ export class Client extends Connector {
|
|
|
172
161
|
this.round++
|
|
173
162
|
|
|
174
163
|
if (this.round % this.options.warn === 1)
|
|
175
|
-
console.warn('Waiting for configuration', {
|
|
164
|
+
console.warn('Waiting for configuration', {
|
|
165
|
+
components: waiting,
|
|
166
|
+
round: this.round
|
|
167
|
+
})
|
|
176
168
|
}
|
|
177
169
|
|
|
178
|
-
private backoff
|
|
170
|
+
private backoff(): number {
|
|
179
171
|
return Math.min(this.options.base * Math.pow(FACTOR, this.round), this.options.max)
|
|
180
172
|
}
|
|
181
173
|
|
|
182
174
|
/** A created object goes to the subscribers of its component and epoch, as it is. */
|
|
183
|
-
private deliver
|
|
175
|
+
private deliver(created: Created): void {
|
|
184
176
|
const listeners = this.listeners.get(id(created.component, created.epoch))
|
|
185
177
|
|
|
186
|
-
if (listeners === undefined)
|
|
187
|
-
return
|
|
178
|
+
if (listeners === undefined) return
|
|
188
179
|
|
|
189
|
-
const value: Value = {
|
|
180
|
+
const value: Value = {
|
|
181
|
+
configuration: created.configuration,
|
|
182
|
+
created: created.CREATED
|
|
183
|
+
}
|
|
190
184
|
|
|
191
|
-
for (const listener of listeners)
|
|
192
|
-
listener(value)
|
|
185
|
+
for (const listener of listeners) listener(value)
|
|
193
186
|
}
|
|
194
187
|
}
|
|
195
188
|
|
|
@@ -197,18 +190,18 @@ export class Client extends Connector {
|
|
|
197
190
|
class Subscription extends Connector {
|
|
198
191
|
private readonly handler: (created: Created) => void
|
|
199
192
|
|
|
200
|
-
public constructor
|
|
193
|
+
public constructor(handler: (created: Created) => void) {
|
|
201
194
|
super()
|
|
202
195
|
|
|
203
196
|
this.handler = handler
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
public async receive
|
|
199
|
+
public async receive(message: Message<Created>): Promise<void> {
|
|
207
200
|
this.handler(message.payload)
|
|
208
201
|
}
|
|
209
202
|
}
|
|
210
203
|
|
|
211
|
-
function id
|
|
204
|
+
function id(component: string, epoch: string): string {
|
|
212
205
|
return component + '\0' + epoch
|
|
213
206
|
}
|
|
214
207
|
|
|
@@ -246,7 +239,7 @@ export interface Created {
|
|
|
246
239
|
component: string
|
|
247
240
|
epoch: string
|
|
248
241
|
configuration: object
|
|
249
|
-
|
|
242
|
+
CREATED: number
|
|
250
243
|
}
|
|
251
244
|
|
|
252
245
|
export type Listener = (value: Value) => void
|
package/source/Composition.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, type Dirent } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import { Connector } from '@toa.io/core'
|
|
4
|
-
import {
|
|
4
|
+
import { uiPort } from './const.js'
|
|
5
5
|
import { UI } from './UI.js'
|
|
6
6
|
import type { Host } from './Factory.js'
|
|
7
7
|
|
|
@@ -9,12 +9,12 @@ import type { Host } from './Factory.js'
|
|
|
9
9
|
export class Composition extends Connector {
|
|
10
10
|
private readonly host: Host
|
|
11
11
|
|
|
12
|
-
public constructor
|
|
12
|
+
public constructor(host: Host) {
|
|
13
13
|
super()
|
|
14
14
|
this.host = host
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
protected override async open
|
|
17
|
+
protected override async open(): Promise<void> {
|
|
18
18
|
const composition = await this.host.composition(components().paths)
|
|
19
19
|
|
|
20
20
|
await composition.connect()
|
|
@@ -23,7 +23,7 @@ export class Composition extends Connector {
|
|
|
23
23
|
|
|
24
24
|
// connected here rather than declared as a dependency: dependencies are connected
|
|
25
25
|
// before `open` runs, so one added from inside it would never be
|
|
26
|
-
const ui = new UI(
|
|
26
|
+
const ui = new UI(uiPort())
|
|
27
27
|
|
|
28
28
|
await ui.connect()
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ export class Composition extends Connector {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function components
|
|
34
|
+
export function components(): Components {
|
|
35
35
|
const labels: string[] = []
|
|
36
36
|
const paths: string[] = []
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ export function components (): Components {
|
|
|
43
43
|
return { labels, paths }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function entries
|
|
46
|
+
function entries(): Dirent[] {
|
|
47
47
|
const entries = readdirSync(ROOT, { withFileTypes: true })
|
|
48
48
|
|
|
49
49
|
return entries.filter((entry) => entry.isDirectory())
|
package/source/Factory.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type Connector, type Locator
|
|
1
|
+
import { type Connector, type Locator } from '@toa.io/core'
|
|
2
|
+
import type { extensions } from '@toa.io/core/types'
|
|
2
3
|
import { Aspect } from './Aspect.js'
|
|
3
4
|
import { Client } from './Client.js'
|
|
4
5
|
import { Composition } from './Composition.js'
|
|
@@ -9,23 +10,22 @@ export class Factory implements extensions.Factory {
|
|
|
9
10
|
private readonly host: Host
|
|
10
11
|
private client: Client | null = null
|
|
11
12
|
|
|
12
|
-
public constructor
|
|
13
|
+
public constructor(host: Host) {
|
|
13
14
|
this.host = host
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
public aspect
|
|
17
|
+
public aspect(locator: Locator, manifest: Manifest): extensions.Aspect {
|
|
17
18
|
const client = overridden(locator) ? null : this.shared()
|
|
18
19
|
|
|
19
20
|
return new Aspect(locator, manifest, client)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
public service
|
|
23
|
+
public service(): Connector {
|
|
23
24
|
return new Composition(this.host)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
private shared
|
|
27
|
-
if (this.client === null || this.client.disposed)
|
|
28
|
-
this.client = new Client(this.host)
|
|
27
|
+
private shared(): Client {
|
|
28
|
+
if (this.client === null || this.client.disposed) this.client = new Client(this.host)
|
|
29
29
|
|
|
30
30
|
return this.client
|
|
31
31
|
}
|
package/source/Secret.test.ts
CHANGED
|
@@ -23,7 +23,7 @@ it('should not show in JSON', () => {
|
|
|
23
23
|
it('should not show when inspected', () => {
|
|
24
24
|
assert.deepStrictEqual(inspect(secret), REDACTED)
|
|
25
25
|
assert.ok(inspect({ key: secret }).includes(REDACTED))
|
|
26
|
-
assert.ok(!
|
|
26
|
+
assert.ok(!inspect({ key: secret }).includes('s3cret'))
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('should not expose the value as a property', () => {
|
package/source/Secret.ts
CHANGED
|
@@ -8,23 +8,23 @@ import { inspect } from 'node:util'
|
|
|
8
8
|
export class Secret {
|
|
9
9
|
readonly #value: string
|
|
10
10
|
|
|
11
|
-
public constructor
|
|
11
|
+
public constructor(value: string) {
|
|
12
12
|
this.#value = value
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
public unwrap
|
|
15
|
+
public unwrap(): string {
|
|
16
16
|
return this.#value
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
public toString
|
|
19
|
+
public toString(): string {
|
|
20
20
|
return REDACTED
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
public toJSON
|
|
23
|
+
public toJSON(): string {
|
|
24
24
|
return REDACTED
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
public [inspect.custom]
|
|
27
|
+
public [inspect.custom](): string {
|
|
28
28
|
return REDACTED
|
|
29
29
|
}
|
|
30
30
|
}
|
package/source/UI.ts
CHANGED
|
@@ -17,7 +17,7 @@ export class UI extends Connector {
|
|
|
17
17
|
private readonly port: number
|
|
18
18
|
private readonly root: string
|
|
19
19
|
|
|
20
|
-
public constructor
|
|
20
|
+
public constructor(port: number, root: string = SITE) {
|
|
21
21
|
super()
|
|
22
22
|
|
|
23
23
|
this.port = port
|
|
@@ -27,7 +27,7 @@ export class UI extends Connector {
|
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
protected override async open
|
|
30
|
+
protected override async open(): Promise<void> {
|
|
31
31
|
/*
|
|
32
32
|
* A taken port is a real error: uniqueness across services is settled at export
|
|
33
33
|
* time, so nothing here has to negotiate for one.
|
|
@@ -54,10 +54,12 @@ export class UI extends Connector {
|
|
|
54
54
|
console.info('Configuration UI started', { port: this.port, path: UI_PATH + '/' })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
protected override async close
|
|
58
|
-
const closed = new Promise<void>((resolve) =>
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
protected override async close(): Promise<void> {
|
|
58
|
+
const closed = new Promise<void>((resolve) =>
|
|
59
|
+
this.server.once('close', () => {
|
|
60
|
+
resolve()
|
|
61
|
+
})
|
|
62
|
+
)
|
|
61
63
|
|
|
62
64
|
this.server.close()
|
|
63
65
|
this.server.closeAllConnections()
|
|
@@ -65,16 +67,18 @@ export class UI extends Connector {
|
|
|
65
67
|
await closed
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
private listen
|
|
70
|
+
private listen(request: http.IncomingMessage, response: http.ServerResponse): void {
|
|
69
71
|
void this.respond(request, response).catch((error: Error) => {
|
|
70
72
|
console.error('Configuration UI failure', { message: error.message })
|
|
71
73
|
|
|
72
|
-
if (!response.writableEnded)
|
|
73
|
-
response.writeHead(500).end()
|
|
74
|
+
if (!response.writableEnded) response.writeHead(500).end()
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
private async respond
|
|
78
|
+
private async respond(
|
|
79
|
+
request: http.IncomingMessage,
|
|
80
|
+
response: http.ServerResponse
|
|
81
|
+
): Promise<void> {
|
|
78
82
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
79
83
|
response.writeHead(405, { allow: 'GET, HEAD' }).end()
|
|
80
84
|
|
|
@@ -98,10 +102,8 @@ export class UI extends Connector {
|
|
|
98
102
|
|
|
99
103
|
const file = this.resolve(pathname)
|
|
100
104
|
|
|
101
|
-
if (file === null)
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
await this.send(file, request, response)
|
|
105
|
+
if (file === null) response.writeHead(404).end()
|
|
106
|
+
else await this.send(file, request, response)
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
/**
|
|
@@ -109,22 +111,18 @@ export class UI extends Connector {
|
|
|
109
111
|
* served as it is; anything else that could be a route falls back to the page,
|
|
110
112
|
* because the client router — not this server — knows what routes there are.
|
|
111
113
|
*/
|
|
112
|
-
private resolve
|
|
113
|
-
if (!pathname.startsWith(UI_PATH))
|
|
114
|
-
return null
|
|
114
|
+
private resolve(pathname: string): string | null {
|
|
115
|
+
if (!pathname.startsWith(UI_PATH)) return null
|
|
115
116
|
|
|
116
117
|
const relative = pathname.slice(UI_PATH.length)
|
|
117
118
|
|
|
118
|
-
if (relative !== '' && !relative.startsWith('/'))
|
|
119
|
-
return null
|
|
119
|
+
if (relative !== '' && !relative.startsWith('/')) return null
|
|
120
120
|
|
|
121
121
|
const file = path.join(this.root, relative)
|
|
122
122
|
|
|
123
|
-
if (file !== this.root && !file.startsWith(this.root + path.sep))
|
|
124
|
-
return null
|
|
123
|
+
if (file !== this.root && !file.startsWith(this.root + path.sep)) return null
|
|
125
124
|
|
|
126
|
-
if (isFile(file))
|
|
127
|
-
return file
|
|
125
|
+
if (isFile(file)) return file
|
|
128
126
|
|
|
129
127
|
/*
|
|
130
128
|
* A missing asset is missing, but a route can look like one: `identity.passkeys` is a
|
|
@@ -137,11 +135,16 @@ export class UI extends Connector {
|
|
|
137
135
|
return asset ? null : path.join(this.root, 'index.html')
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
private async send
|
|
138
|
+
private async send(
|
|
139
|
+
file: string,
|
|
140
|
+
request: http.IncomingMessage,
|
|
141
|
+
response: http.ServerResponse
|
|
142
|
+
): Promise<void> {
|
|
141
143
|
const stats = await fs.promises.stat(file).catch(() => null)
|
|
142
144
|
|
|
143
145
|
if (stats === null) {
|
|
144
|
-
response
|
|
146
|
+
response
|
|
147
|
+
.writeHead(503, { 'content-type': 'text/plain' })
|
|
145
148
|
.end('The configuration UI is not built. Run `npm run build:ui`.\n')
|
|
146
149
|
|
|
147
150
|
return
|
|
@@ -153,14 +156,12 @@ export class UI extends Connector {
|
|
|
153
156
|
'cache-control': caching(path.relative(this.root, file))
|
|
154
157
|
})
|
|
155
158
|
|
|
156
|
-
if (request.method === 'HEAD')
|
|
157
|
-
|
|
158
|
-
else
|
|
159
|
-
fs.createReadStream(file).pipe(response)
|
|
159
|
+
if (request.method === 'HEAD') response.end()
|
|
160
|
+
else fs.createReadStream(file).pipe(response)
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function decode
|
|
164
|
+
function decode(url: string): string | null {
|
|
164
165
|
try {
|
|
165
166
|
return decodeURIComponent(url.split('?')[0])
|
|
166
167
|
} catch {
|
|
@@ -168,7 +169,7 @@ function decode (url: string): string | null {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function isFile
|
|
172
|
+
function isFile(file: string): boolean {
|
|
172
173
|
return fs.existsSync(file) && fs.statSync(file).isFile()
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -188,7 +189,7 @@ const DAY = 'public, max-age=86400'
|
|
|
188
189
|
* and their names carry the build. An icon is named for what it is rather than for its
|
|
189
190
|
* content, so it is asked about again, but not on every page load.
|
|
190
191
|
*/
|
|
191
|
-
function caching
|
|
192
|
+
function caching(relative: string): string {
|
|
192
193
|
if (relative.startsWith(IMMUTABLE)) return FOREVER
|
|
193
194
|
|
|
194
195
|
return ICONS.has(relative) ? DAY : 'no-cache'
|
|
@@ -21,8 +21,7 @@ beforeEach(() => {
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
afterEach(() => {
|
|
24
|
-
for (const name of used)
|
|
25
|
-
delete process.env[name]
|
|
24
|
+
for (const name of used) delete process.env[name]
|
|
26
25
|
|
|
27
26
|
used = []
|
|
28
27
|
})
|
|
@@ -92,14 +91,21 @@ describe('local', () => {
|
|
|
92
91
|
manifest.schema = {
|
|
93
92
|
type: 'object',
|
|
94
93
|
properties: {
|
|
95
|
-
foo: { type: 'string'
|
|
94
|
+
foo: { type: 'string' },
|
|
96
95
|
bar: { type: 'number' }
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
set({ bar: 5 })
|
|
101
100
|
|
|
102
|
-
assert.deepStrictEqual(local(locator, manifest), {
|
|
101
|
+
assert.deepStrictEqual(local(locator, manifest), { bar: 5 })
|
|
102
|
+
|
|
103
|
+
set({ bar: 'five' })
|
|
104
|
+
|
|
105
|
+
assert.throws(
|
|
106
|
+
() => local(locator, manifest),
|
|
107
|
+
(error: Error) => /must be number/.test(error.message)
|
|
108
|
+
)
|
|
103
109
|
})
|
|
104
110
|
})
|
|
105
111
|
|
|
@@ -109,18 +115,18 @@ describe('fit', () => {
|
|
|
109
115
|
type: 'object',
|
|
110
116
|
properties: {
|
|
111
117
|
foo: { type: 'string' },
|
|
112
|
-
bar: { type: 'number'
|
|
118
|
+
bar: { type: 'number' }
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
set('secret', '_FOO')
|
|
117
123
|
|
|
118
|
-
const raw = { foo: '$FOO' }
|
|
124
|
+
const raw = { foo: '$FOO', bar: 1 }
|
|
119
125
|
const values = fit(raw, manifest)
|
|
120
126
|
|
|
121
127
|
assert.deepStrictEqual((values.foo as Secret).unwrap(), 'secret')
|
|
122
128
|
assert.deepStrictEqual(values.bar, 1)
|
|
123
|
-
assert.deepStrictEqual(raw, { foo: '$FOO' }) // untouched
|
|
129
|
+
assert.deepStrictEqual(raw, { foo: '$FOO', bar: 1 }) // untouched
|
|
124
130
|
})
|
|
125
131
|
|
|
126
132
|
it('should not apply the manifest defaults', async () => {
|
|
@@ -134,7 +140,7 @@ describe('fit', () => {
|
|
|
134
140
|
})
|
|
135
141
|
})
|
|
136
142
|
|
|
137
|
-
function set
|
|
143
|
+
function set(value: object | string, key = locator.uppercase): void {
|
|
138
144
|
const string = typeof value === 'string' ? value : JSON.stringify(value)
|
|
139
145
|
const name = 'TOA_CONFIGURATION_' + key
|
|
140
146
|
|
package/source/configuration.ts
CHANGED
|
@@ -7,22 +7,21 @@ import type { Schema } from '@toa.io/schemas'
|
|
|
7
7
|
import type { Manifest } from './manifest.js'
|
|
8
8
|
|
|
9
9
|
/** The variable is set, so the values service is not consulted. */
|
|
10
|
-
export function overridden
|
|
10
|
+
export function overridden(locator: Locator): boolean {
|
|
11
11
|
return process.env[PREFIX + locator.uppercase] !== undefined
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** The variable, the manifest defaults, then the schema. */
|
|
15
|
-
export function local
|
|
15
|
+
export function local(locator: Locator, manifest: Manifest): Node {
|
|
16
16
|
const values = read(locator.uppercase)
|
|
17
17
|
|
|
18
|
-
if (manifest.defaults !== undefined)
|
|
19
|
-
add(values, manifest.defaults)
|
|
18
|
+
if (manifest.defaults !== undefined) add(values, manifest.defaults)
|
|
20
19
|
|
|
21
20
|
return fit(values, manifest)
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
/** A copy of the value with the schema applied and the secrets substituted. */
|
|
25
|
-
export function fit
|
|
24
|
+
export function fit(raw: object, manifest: Manifest): Node {
|
|
26
25
|
// a copy of the caller's, and one of this realm: what came over the wire is JSON anyway
|
|
27
26
|
const values = JSON.parse(JSON.stringify(raw)) as Node
|
|
28
27
|
|
|
@@ -33,26 +32,23 @@ export function fit (raw: object, manifest: Manifest): Node {
|
|
|
33
32
|
return values
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
function validate
|
|
35
|
+
function validate(values: Node, manifest: Manifest): void {
|
|
37
36
|
const schema: Schema<any> = schemas.schema(manifest.schema)
|
|
38
37
|
|
|
39
38
|
schema.validate(values)
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
function read
|
|
41
|
+
function read(suffix: string): Node {
|
|
43
42
|
const variable = PREFIX + suffix
|
|
44
43
|
const string = process.env[variable]
|
|
45
44
|
|
|
46
|
-
if (string === undefined)
|
|
47
|
-
|
|
48
|
-
else
|
|
49
|
-
return JSON.parse(string)
|
|
45
|
+
if (string === undefined) return {}
|
|
46
|
+
else return JSON.parse(string)
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
function substituteSecrets
|
|
49
|
+
function substituteSecrets(configuration: Node): void {
|
|
53
50
|
for (const [key, value] of Object.entries(configuration)) {
|
|
54
|
-
if (typeof value === 'object' && value !== null)
|
|
55
|
-
substituteSecrets(value as Node)
|
|
51
|
+
if (typeof value === 'object' && value !== null) substituteSecrets(value as Node)
|
|
56
52
|
|
|
57
53
|
if (typeof value !== 'string') continue
|
|
58
54
|
|
|
@@ -66,7 +62,7 @@ function substituteSecrets (configuration: Node): void {
|
|
|
66
62
|
}
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
function getSecret
|
|
65
|
+
function getSecret(name: string): string {
|
|
70
66
|
const variable = PREFIX + '_' + name
|
|
71
67
|
const value = process.env[variable]
|
|
72
68
|
|
package/source/const.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Source } from '@toa.io/core'
|
|
1
|
+
import type { Source } from '@toa.io/core/types'
|
|
2
2
|
|
|
3
3
|
/** Per-component variables: the local override and the secrets. */
|
|
4
4
|
export const PREFIX = 'TOA_CONFIGURATION_'
|
|
@@ -12,6 +12,18 @@ export const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
|
|
|
12
12
|
export const UI_PATH = '/.configuration'
|
|
13
13
|
export const UI_PORT = 8003
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The port the UI is served on. `UI_PORT` is what a deployment publishes and what the chart
|
|
17
|
+
* renders; the variable is for a machine that runs more than one Toa process — a Toa checkout
|
|
18
|
+
* beside an application, or two applications — where one of them has to give it up.
|
|
19
|
+
*/
|
|
20
|
+
export const UI_PORT_ENV = 'TOA_CONFIGURATION_UI_PORT'
|
|
21
|
+
export function uiPort(): number {
|
|
22
|
+
const value = process.env[UI_PORT_ENV]
|
|
23
|
+
|
|
24
|
+
return value === undefined ? UI_PORT : Number(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export const EVENT = 'configuration.values.created'
|
|
16
28
|
|
|
17
29
|
export const SOURCE: Source = { service: 'configuration' }
|
package/source/context.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Manifest } from './manifest.js'
|
|
2
|
-
import type { Contribution } from '@toa.io/core'
|
|
2
|
+
import type { Contribution } from '@toa.io/core/types'
|
|
3
3
|
|
|
4
4
|
/** What this extension puts on the context of a component that declares it. */
|
|
5
|
-
export function context
|
|
5
|
+
export function context(declaration: Manifest): Contribution {
|
|
6
6
|
const contribution: Contribution = { name: 'configuration', schema: declaration.schema }
|
|
7
7
|
|
|
8
8
|
// a value the schema declares a secret is one on the context, not a string
|