@toa.io/extensions.exposition 1.0.0-alpha.278 → 1.0.0-alpha.282
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 +31 -0
- package/components/identity.basic/events/principal.js +5 -1
- package/components/identity.basic/manifest.toa.yaml +11 -1
- package/components/identity.basic/operations/authenticate.js +9 -5
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/check.js +2 -1
- package/components/identity.basic/operations/check.js.map +1 -1
- package/components/identity.basic/operations/incept.js +4 -5
- package/components/identity.basic/operations/incept.js.map +1 -1
- package/components/identity.basic/operations/lib/credentials.d.ts +5 -0
- package/components/identity.basic/operations/lib/credentials.js +12 -0
- package/components/identity.basic/operations/lib/credentials.js.map +1 -0
- package/components/identity.basic/operations/transit.d.ts +2 -0
- package/components/identity.basic/operations/transit.js +12 -2
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/types.d.ts +11 -1
- package/components/identity.basic/source/authenticate.ts +11 -5
- package/components/identity.basic/source/check.ts +2 -1
- package/components/identity.basic/source/incept.ts +5 -5
- package/components/identity.basic/source/lib/credentials.test.ts +18 -0
- package/components/identity.basic/source/lib/credentials.ts +13 -0
- package/components/identity.basic/source/transit.ts +16 -4
- package/components/identity.basic/source/types.ts +13 -1
- package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/events/principal.js +1 -0
- package/components/identity.federation/manifest.toa.yaml +5 -1
- package/components/identity.federation/operations/authenticate.js +3 -3
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/create.js +5 -1
- package/components/identity.federation/operations/create.js.map +1 -1
- package/components/identity.federation/operations/lib/decode.js +5 -1
- package/components/identity.federation/operations/lib/decode.js.map +1 -1
- package/components/identity.federation/operations/lib/exchange.js +3 -3
- package/components/identity.federation/operations/lib/exchange.js.map +1 -1
- package/components/identity.federation/operations/list.js +2 -1
- package/components/identity.federation/operations/list.js.map +1 -1
- package/components/identity.federation/operations/types/configuration.d.ts +2 -1
- package/components/identity.federation/source/authenticate.ts +3 -3
- package/components/identity.federation/source/create.ts +5 -1
- package/components/identity.federation/source/lib/decode.ts +5 -1
- package/components/identity.federation/source/lib/exchange.ts +3 -3
- package/components/identity.federation/source/list.test.ts +1 -1
- package/components/identity.federation/source/list.ts +2 -1
- package/components/identity.federation/source/types/configuration.ts +2 -1
- package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/manifest.toa.yaml +18 -1
- package/components/identity.keys/operations/disable.d.ts +5 -0
- package/components/identity.keys/operations/disable.js +5 -0
- package/components/identity.keys/operations/disable.js.map +1 -0
- package/components/identity.keys/operations/revoke.d.ts +15 -0
- package/components/identity.keys/operations/revoke.js +12 -0
- package/components/identity.keys/operations/revoke.js.map +1 -0
- package/components/identity.keys/receivers/identity.bans.created.js +1 -0
- package/components/identity.keys/receivers/identity.bans.updated.js +1 -0
- package/components/identity.keys/receivers/identity.roles.deleted.js +1 -0
- package/components/identity.keys/source/disable.ts +9 -0
- package/components/identity.keys/source/revoke.ts +30 -0
- package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/manifest.toa.yaml +6 -0
- package/components/identity.otp/operations/authenticate.js +13 -1
- package/components/identity.otp/operations/authenticate.js.map +1 -1
- package/components/identity.otp/operations/issue.js +3 -2
- package/components/identity.otp/operations/issue.js.map +1 -1
- package/components/identity.otp/operations/lib/Context.d.ts +1 -0
- package/components/identity.otp/source/authenticate.ts +19 -1
- package/components/identity.otp/source/issue.ts +3 -2
- package/components/identity.otp/source/lib/Context.ts +1 -0
- package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/operations/authenticate.js +2 -1
- package/components/identity.passkeys/operations/authenticate.js.map +1 -1
- package/components/identity.passkeys/operations/challenge.js +3 -3
- package/components/identity.passkeys/operations/challenge.js.map +1 -1
- package/components/identity.passkeys/operations/delete.js +2 -1
- package/components/identity.passkeys/operations/delete.js.map +1 -1
- package/components/identity.passkeys/operations/list.js +2 -1
- package/components/identity.passkeys/operations/list.js.map +1 -1
- package/components/identity.passkeys/source/authenticate.ts +2 -1
- package/components/identity.passkeys/source/challenge.ts +3 -3
- package/components/identity.passkeys/source/delete.ts +2 -1
- package/components/identity.passkeys/source/list.ts +2 -1
- package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/manifest.toa.yaml +4 -0
- package/components/identity.roles/operations/grant.js +5 -2
- package/components/identity.roles/operations/grant.js.map +1 -1
- package/components/identity.roles/source/grant.ts +6 -2
- package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/manifest.toa.yaml +3 -0
- package/components/identity.tokens/operations/authenticate.js +4 -3
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.js +7 -1
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.js +3 -2
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/issue.js +2 -1
- package/components/identity.tokens/operations/issue.js.map +1 -1
- package/components/identity.tokens/operations/lib/types.d.ts +3 -0
- package/components/identity.tokens/source/authenticate.test.ts +26 -2
- package/components/identity.tokens/source/authenticate.ts +4 -3
- package/components/identity.tokens/source/decrypt.test.ts +35 -1
- package/components/identity.tokens/source/decrypt.ts +10 -1
- package/components/identity.tokens/source/encrypt.test.ts +15 -0
- package/components/identity.tokens/source/encrypt.ts +4 -2
- package/components/identity.tokens/source/issue.ts +2 -1
- package/components/identity.tokens/source/lib/types.ts +4 -0
- package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
- package/documentation/authorities.md +3 -0
- package/documentation/components.md +31 -11
- package/documentation/identity.md +38 -6
- package/documentation/io.md +2 -2
- package/documentation/ip.md +24 -0
- package/documentation/passkeys.md +6 -0
- package/features/auth.assert.feature +2 -4
- package/features/auth.claims.feature +1 -0
- package/features/auth.incept.feature +46 -0
- package/features/authorities.feature +39 -0
- package/features/authorities.federation.feature +1 -0
- package/features/identity.bans.feature +63 -0
- package/features/identity.basic.feature +95 -3
- package/features/identity.feature +151 -0
- package/features/identity.federation.feature +5 -0
- package/features/identity.otp.feature +41 -0
- package/features/identity.roles.feature +252 -0
- package/features/identity.tokens.custom.feature +145 -0
- package/features/map.feature +2 -0
- package/features/steps/Gateway.ts +8 -2
- package/features/steps/Realtime.ts +1 -1
- package/package.json +5 -5
- package/readme.md +3 -0
- package/schemas/annotation.cos.yaml +16 -0
- package/source/Annotation.ts +14 -0
- package/source/Composition.ts +5 -5
- package/source/Directive.test.ts +3 -1
- package/source/Directive.ts +6 -2
- package/source/Endpoint.ts +4 -2
- package/source/Factory.ts +12 -9
- package/source/HTTP/Context.ts +12 -2
- package/source/HTTP/Server.ts +16 -3
- package/source/HTTP/address.test.ts +22 -0
- package/source/HTTP/address.ts +17 -0
- package/source/Query.test.ts +15 -1
- package/source/Query.ts +3 -1
- package/source/RTD/Directives.ts +10 -1
- package/source/Remotes.test.ts +7 -12
- package/source/Remotes.ts +5 -5
- package/source/const.ts +3 -0
- package/source/deployment.ts +7 -1
- package/source/directives/auth/Assert.ts +3 -0
- package/source/directives/auth/Authorization.test.ts +24 -0
- package/source/directives/auth/Authorization.ts +80 -11
- package/source/directives/auth/Incept.ts +9 -4
- package/source/directives/auth/Role.test.ts +1 -1
- package/source/directives/auth/Role.ts +2 -1
- package/source/directives/auth/schemes.test.ts +14 -0
- package/source/directives/auth/schemes.ts +14 -0
- package/source/directives/auth/types.ts +3 -0
- package/source/directives/io/IO.ts +10 -3
- package/source/directives/io/lib/throttle/Keys.ts +10 -3
- package/source/directives/io/lib/throttle/Quotas.test.ts +14 -4
- package/source/directives/io/lib/throttle/Quotas.ts +15 -2
- package/source/directives/io/lib/throttle/Sync.test.ts +7 -9
- package/source/directives/io/lib/throttle/Sync.ts +4 -7
- package/source/directives/io/lib/throttle/Warning.test.ts +32 -0
- package/source/directives/io/lib/throttle/Warning.ts +30 -0
- package/source/directives/io/lib/throttle/components/Component.ts +2 -1
- package/source/directives/io/lib/throttle/components/IP.ts +19 -28
- package/source/directives/io/lib/throttle/index.ts +1 -0
- package/source/redact.test.ts +27 -0
- package/source/redact.ts +21 -0
- package/source/schemas.test.ts +17 -0
- package/transpiled/Annotation.d.ts +10 -0
- package/transpiled/Composition.d.ts +3 -3
- package/transpiled/Composition.js +4 -4
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Directive.d.ts +3 -2
- package/transpiled/Directive.js +3 -1
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Endpoint.js +3 -2
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.d.ts +3 -3
- package/transpiled/Factory.js +11 -8
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +4 -0
- package/transpiled/HTTP/Context.js +7 -2
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +5 -0
- package/transpiled/HTTP/Server.js +8 -3
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/address.d.ts +7 -0
- package/transpiled/HTTP/address.js +14 -0
- package/transpiled/HTTP/address.js.map +1 -0
- package/transpiled/Query.js +3 -1
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +9 -1
- package/transpiled/Remotes.d.ts +3 -3
- package/transpiled/Remotes.js +4 -4
- package/transpiled/Remotes.js.map +1 -1
- package/transpiled/const.d.ts +2 -0
- package/transpiled/const.js +2 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/deployment.js +4 -0
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/Assert.js +2 -0
- package/transpiled/directives/auth/Assert.js.map +1 -1
- package/transpiled/directives/auth/Authorization.d.ts +8 -0
- package/transpiled/directives/auth/Authorization.js +61 -11
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/auth/Incept.d.ts +3 -1
- package/transpiled/directives/auth/Incept.js +8 -4
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/directives/auth/Role.js +2 -1
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/auth/schemes.d.ts +10 -0
- package/transpiled/directives/auth/schemes.js +12 -0
- package/transpiled/directives/auth/schemes.js.map +1 -1
- package/transpiled/directives/auth/types.d.ts +2 -0
- package/transpiled/directives/io/IO.d.ts +4 -1
- package/transpiled/directives/io/IO.js +7 -3
- package/transpiled/directives/io/IO.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Keys.d.ts +2 -1
- package/transpiled/directives/io/lib/throttle/Keys.js +7 -2
- package/transpiled/directives/io/lib/throttle/Keys.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Quotas.d.ts +3 -2
- package/transpiled/directives/io/lib/throttle/Quotas.js +8 -1
- package/transpiled/directives/io/lib/throttle/Quotas.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Sync.d.ts +2 -3
- package/transpiled/directives/io/lib/throttle/Sync.js +1 -3
- package/transpiled/directives/io/lib/throttle/Sync.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/Warning.d.ts +11 -0
- package/transpiled/directives/io/lib/throttle/Warning.js +24 -0
- package/transpiled/directives/io/lib/throttle/Warning.js.map +1 -0
- package/transpiled/directives/io/lib/throttle/components/Component.d.ts +2 -1
- package/transpiled/directives/io/lib/throttle/components/IP.d.ts +10 -2
- package/transpiled/directives/io/lib/throttle/components/IP.js +18 -23
- package/transpiled/directives/io/lib/throttle/components/IP.js.map +1 -1
- package/transpiled/directives/io/lib/throttle/index.d.ts +1 -0
- package/transpiled/directives/io/lib/throttle/index.js +1 -0
- package/transpiled/directives/io/lib/throttle/index.js.map +1 -1
- package/transpiled/redact.d.ts +5 -0
- package/transpiled/redact.js +17 -0
- package/transpiled/redact.js.map +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/components/exposition.atom/manifest.toa.yaml +0 -21
- package/components/exposition.atom/operations/meter.js +0 -5
package/source/Factory.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { families, interceptors } from './directives/index.js'
|
|
|
10
10
|
import { DirectivesFactory } from './Directive.js'
|
|
11
11
|
import { Composition } from './Composition.js'
|
|
12
12
|
import * as root from './root.js'
|
|
13
|
+
import { ATOM_GROUP } from './const.js'
|
|
13
14
|
import { Interception } from './Interception.js'
|
|
14
15
|
import * as http from './HTTP/index.js'
|
|
15
16
|
import type { Branch } from './Branch.js'
|
|
@@ -18,14 +19,14 @@ import type { Broadcast } from './Gateway.js'
|
|
|
18
19
|
import type { Connector, Locator, extensions } from '@toa.io/core'
|
|
19
20
|
|
|
20
21
|
export class Factory implements extensions.Factory {
|
|
21
|
-
private readonly
|
|
22
|
+
private readonly host: Host
|
|
22
23
|
|
|
23
|
-
public constructor (
|
|
24
|
-
this.
|
|
24
|
+
public constructor (host: Host) {
|
|
25
|
+
this.host = host
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
public async tenant (locator: Locator, node: syntax.Node): Promise<Connector> {
|
|
28
|
-
const broadcast: Broadcast = await this.
|
|
29
|
+
const broadcast: Broadcast = await this.host.broadcast(CHANNEL, locator.id)
|
|
29
30
|
const hash = createHash('sha256').update(JSON.stringify(node)).digest('hex')
|
|
30
31
|
|
|
31
32
|
// no timestamp: the tenant stamps each announcement with its own start time
|
|
@@ -47,20 +48,22 @@ export class Factory implements extensions.Factory {
|
|
|
47
48
|
configureLogs()
|
|
48
49
|
|
|
49
50
|
const options = JSON.parse(process.env.TOA_EXPOSITION_PROPERTIES) as http.Options
|
|
50
|
-
const broadcast: Broadcast = await this.
|
|
51
|
+
const broadcast: Broadcast = await this.host.broadcast(CHANNEL)
|
|
51
52
|
const server = http.Server.create({ ...options })
|
|
52
|
-
const remotes = new Remotes(this.
|
|
53
|
+
const remotes = new Remotes(this.host)
|
|
53
54
|
const node = root.resolve()
|
|
54
55
|
const methods = new EndpointsFactory(remotes)
|
|
55
|
-
const directives = new DirectivesFactory(families, remotes)
|
|
56
|
+
const directives = new DirectivesFactory(families, remotes, this.host, options)
|
|
56
57
|
const interception = new Interception(interceptors)
|
|
57
58
|
const tree = new Tree(node, methods, directives)
|
|
58
59
|
|
|
59
|
-
const composition = new Composition(this.
|
|
60
|
+
const composition = new Composition(this.host)
|
|
60
61
|
const gateway = new Gateway(broadcast, tree, interception)
|
|
61
62
|
|
|
62
63
|
gateway.depends(remotes)
|
|
63
64
|
gateway.depends(composition)
|
|
65
|
+
// what the directives meter through; one atom per process, connected once
|
|
66
|
+
gateway.depends(this.host.atom(ATOM_GROUP))
|
|
64
67
|
|
|
65
68
|
server.attach(gateway.process.bind(gateway))
|
|
66
69
|
server.depends(gateway)
|
|
@@ -101,4 +104,4 @@ function development (): TracesOptions {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
104
|
-
export type
|
|
107
|
+
export type Host = extensions.Host
|
package/source/HTTP/Context.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { console } from 'openspan'
|
|
|
5
5
|
import { Timing } from './Timing.js'
|
|
6
6
|
import { type Format, formats, types } from './formats/index.js'
|
|
7
7
|
import { read } from './messages.js'
|
|
8
|
+
import { address } from './address.js'
|
|
8
9
|
import type { OutgoingMessage } from './messages.js'
|
|
9
10
|
import type { IncomingMessage } from './types.js'
|
|
10
11
|
|
|
@@ -12,6 +13,9 @@ export class Context {
|
|
|
12
13
|
public readonly id: string
|
|
13
14
|
public readonly authority: string
|
|
14
15
|
public readonly request: IncomingMessage
|
|
16
|
+
|
|
17
|
+
/** the client address, read as the deployment says; none otherwise, see `documentation/ip.md` */
|
|
18
|
+
public readonly ip: string | undefined
|
|
15
19
|
public readonly url: URL
|
|
16
20
|
public readonly subtype: string | null = null
|
|
17
21
|
public readonly encoder: Format | null = null
|
|
@@ -30,6 +34,7 @@ export class Context {
|
|
|
30
34
|
url: URL) {
|
|
31
35
|
this.authority = authority
|
|
32
36
|
this.request = request
|
|
37
|
+
this.ip = address(request, properties.ip)
|
|
33
38
|
|
|
34
39
|
this.id = crypto.randomUUID()
|
|
35
40
|
// parsed by the server, which had to parse it anyway to know the request is valid
|
|
@@ -80,13 +85,15 @@ export class Context {
|
|
|
80
85
|
const headers = { ...request.headers }
|
|
81
86
|
|
|
82
87
|
if (headers.authorization !== undefined)
|
|
83
|
-
|
|
84
|
-
headers.authorization = headers.authorization.slice(0, headers.authorization.indexOf(' '))
|
|
88
|
+
headers.authorization = SCHEME.exec(headers.authorization)?.[1] ?? '[malformed]'
|
|
85
89
|
|
|
86
90
|
console.debug('Received request', { method: request.method, url: request.url, headers })
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
/** The scheme of an `authorization` header; the value after it is a credential. */
|
|
95
|
+
const SCHEME = /^(\w{1,32})(?: |$)/
|
|
96
|
+
|
|
90
97
|
interface Pipelines {
|
|
91
98
|
body: Array<(input: unknown) => unknown>
|
|
92
99
|
response: Array<(output: OutgoingMessage) => void | Promise<void>>
|
|
@@ -94,6 +101,9 @@ interface Pipelines {
|
|
|
94
101
|
|
|
95
102
|
interface Properties {
|
|
96
103
|
debug: boolean
|
|
104
|
+
|
|
105
|
+
/** the header the client address is read from; the connection's without one */
|
|
106
|
+
ip?: string
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/**
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ClientError, Exception } from './exceptions.js'
|
|
|
12
12
|
import { Context } from './Context.js'
|
|
13
13
|
import { PROBE, Probe } from './Probe.js'
|
|
14
14
|
import type { IncomingMessage, Protocol, ServerResponse } from './types.js'
|
|
15
|
+
import type { Bouncer } from '../Annotation.js'
|
|
15
16
|
|
|
16
17
|
export class Server extends Connector {
|
|
17
18
|
private readonly server: http.Server | http2.Http2Server
|
|
@@ -129,10 +130,10 @@ export class Server extends Connector {
|
|
|
129
130
|
// no listener on `request.socket`: under HTTP/2 it is the session's socket, shared by
|
|
130
131
|
// every concurrent stream, and removing listeners on it would strip the siblings'
|
|
131
132
|
|
|
132
|
-
const host = authorityOf(request)
|
|
133
|
+
const host = authorityOf(request)?.toLowerCase()
|
|
133
134
|
|
|
134
|
-
if (host === undefined) {
|
|
135
|
-
console.warn('Request without
|
|
135
|
+
if (host === undefined || !HOST.test(host)) {
|
|
136
|
+
console.warn('Request without a valid authority', errorAttributes(request, new Error('Invalid authority')))
|
|
136
137
|
|
|
137
138
|
response.writeHead(400).end()
|
|
138
139
|
|
|
@@ -350,6 +351,12 @@ function trace (headers: IncomingMessage['headers']): SpanContext | null {
|
|
|
350
351
|
return null
|
|
351
352
|
}
|
|
352
353
|
|
|
354
|
+
/**
|
|
355
|
+
* A host name with an optional port, or a bracketed IPv6 literal. The URL parser admits
|
|
356
|
+
* `,` `;` `=` `(` `)` and quotes in a host, and the authority is written into criteria.
|
|
357
|
+
*/
|
|
358
|
+
const HOST = /^(?:[a-z0-9.-]{1,253}|\[[0-9a-f:.]{2,45}\])(?::\d{1,5})?$/
|
|
359
|
+
|
|
353
360
|
const RAY = /^[\da-f]{32}$/i
|
|
354
361
|
const ZERO_RAY = '0'.repeat(32)
|
|
355
362
|
|
|
@@ -372,6 +379,12 @@ interface Properties {
|
|
|
372
379
|
|
|
373
380
|
/** Port of the readiness probe, which is HTTP/1.1 whatever the gateway serves. */
|
|
374
381
|
probe: number
|
|
382
|
+
|
|
383
|
+
/** The header the client address is read from; the connection's address without one. */
|
|
384
|
+
ip?: string
|
|
385
|
+
|
|
386
|
+
/** Failed authentications an address may make; none are metered unless set. */
|
|
387
|
+
bouncer?: Bouncer
|
|
375
388
|
}
|
|
376
389
|
|
|
377
390
|
export type Options = { authorities: Properties['authorities'] } & {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { address } from './address.js'
|
|
5
|
+
import type { IncomingMessage } from './types.js'
|
|
6
|
+
|
|
7
|
+
const request = (headers: Record<string, string>): IncomingMessage =>
|
|
8
|
+
({ headers, socket: { remoteAddress: '9.9.9.9' } }) as unknown as IncomingMessage
|
|
9
|
+
|
|
10
|
+
it('should be nothing without a header named', () => {
|
|
11
|
+
assert.equal(address(request({ 'x-forwarded-for': '1.1.1.1' })), undefined)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should be the last value of the named header', () => {
|
|
15
|
+
assert.equal(address(request({ 'x-forwarded-for': '1.1.1.1, 2.2.2.2' }), 'x-forwarded-for'), '2.2.2.2')
|
|
16
|
+
assert.equal(address(request({ 'cf-connecting-ip': '3.3.3.3' }), 'cf-connecting-ip'), '3.3.3.3')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should be nothing when the named header is absent or empty', () => {
|
|
20
|
+
assert.equal(address(request({}), 'x-real-ip'), undefined)
|
|
21
|
+
assert.equal(address(request({ 'x-real-ip': ' ' }), 'x-real-ip'), undefined)
|
|
22
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The client address: the value of the header the deployment names, and nothing otherwise.
|
|
5
|
+
* Of a header holding a list, the last value is the one the edge in front appended. The
|
|
6
|
+
* connection's address is never it: behind an edge it is the edge's.
|
|
7
|
+
*/
|
|
8
|
+
export function address (request: IncomingMessage, header?: string): string | undefined {
|
|
9
|
+
if (header === undefined)
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
const value = request.headers[header]
|
|
13
|
+
const raw = Array.isArray(value) ? value[value.length - 1] : value
|
|
14
|
+
const last = raw?.slice(raw.lastIndexOf(',') + 1).trim()
|
|
15
|
+
|
|
16
|
+
return last === '' ? undefined : last
|
|
17
|
+
}
|
package/source/Query.test.ts
CHANGED
|
@@ -19,7 +19,21 @@ it('should combine request criteria', async () => {
|
|
|
19
19
|
const instance = new Query(query)
|
|
20
20
|
const result = instance.fit({ criteria: 'qux==4' }, parameters)
|
|
21
21
|
|
|
22
|
-
assert.deepStrictEqual(result.query!.criteria, '(bar==2;baz==3);(foo==1);(qux==4)')
|
|
22
|
+
assert.deepStrictEqual(result.query!.criteria, '(bar=="2";baz=="3");(foo==1);(qux==4)')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should quote parameter values', async () => {
|
|
26
|
+
const query: syntax.Query = {
|
|
27
|
+
omit: { range: [0, 1] },
|
|
28
|
+
limit: { range: [0, 1] }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parameters: Parameter[] = [{ name: 'user', value: 'me"),(user=="victim' }]
|
|
32
|
+
|
|
33
|
+
const instance = new Query(query)
|
|
34
|
+
const result = instance.fit({}, parameters)
|
|
35
|
+
|
|
36
|
+
assert.deepStrictEqual(result.query!.criteria, '(user=="me\\"),(user==\\"victim")')
|
|
23
37
|
})
|
|
24
38
|
|
|
25
39
|
it('should set id parameter as query.id', async () => {
|
package/source/Query.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
+
import { quote } from '@toa.io/generic'
|
|
2
3
|
import * as http from './HTTP/index.js'
|
|
3
4
|
import { type Parameter } from './RTD/index.js'
|
|
4
5
|
import * as schemas from './schemas.js'
|
|
@@ -132,8 +133,9 @@ export class Query {
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
if (parameters.length > 0) {
|
|
136
|
+
// a segment may carry `,` `;` `(` `)` `=`, which is criteria grammar when unquoted
|
|
135
137
|
const criteria = parameters
|
|
136
|
-
.map(({ name, value }) => `${name}==${value}`)
|
|
138
|
+
.map(({ name, value }) => `${name}==${quote(value)}`)
|
|
137
139
|
.join(';')
|
|
138
140
|
|
|
139
141
|
groups.push({ criteria, operator: this.prepend })
|
package/source/RTD/Directives.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Parameter } from './Match.js'
|
|
2
2
|
import type * as syntax from './syntax/index.js'
|
|
3
|
-
import type { Context, OutgoingMessage } from '../HTTP/index.js'
|
|
3
|
+
import type { Context, OutgoingMessage, Options } from '../HTTP/index.js'
|
|
4
4
|
import type { Output } from '../io.js'
|
|
5
|
+
import type { extensions } from '@toa.io/core'
|
|
6
|
+
|
|
7
|
+
type Host = extensions.Host
|
|
5
8
|
|
|
6
9
|
export interface Directives {
|
|
7
10
|
preflight: (context: Context, parameters: Parameter[]) => Promise<Output>
|
|
@@ -28,6 +31,12 @@ export interface DirectiveFamily<TDirective = any, TExtension = any> {
|
|
|
28
31
|
|
|
29
32
|
create: (name: string, ...rest: any[]) => TDirective
|
|
30
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Called once per gateway with what the host provides and how the gateway is configured,
|
|
36
|
+
* before any directive is created.
|
|
37
|
+
*/
|
|
38
|
+
mount?: (host: Host, options: Options) => void
|
|
39
|
+
|
|
31
40
|
/**
|
|
32
41
|
* Puts the directives of one route in the order they must run in. Called once, when
|
|
33
42
|
* the set is built — the order cannot depend on the request.
|
package/source/Remotes.test.ts
CHANGED
|
@@ -5,19 +5,14 @@ import { isDeepStrictEqual } from 'node:util'
|
|
|
5
5
|
import { generate } from 'randomstring'
|
|
6
6
|
import { Connector } from '@toa.io/core'
|
|
7
7
|
import { Remotes } from './Remotes.js'
|
|
8
|
-
import type {
|
|
9
|
-
import type { Locator, Source } from '@toa.io/core'
|
|
8
|
+
import type { Host } from './Factory.js'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
remote: async (locator: Locator, source?: Source) => await boot.remote(locator, source)
|
|
13
|
-
}) })
|
|
14
|
-
|
|
15
|
-
const boot = {
|
|
10
|
+
const host = {
|
|
16
11
|
remote: mock.fn(async () => ({
|
|
17
12
|
connect: mock.fn(() => undefined),
|
|
18
13
|
link: mock.fn(() => undefined)
|
|
19
14
|
}))
|
|
20
|
-
} as unknown as
|
|
15
|
+
} as unknown as Host
|
|
21
16
|
|
|
22
17
|
const namespace = generate()
|
|
23
18
|
const name = generate()
|
|
@@ -25,15 +20,15 @@ const name = generate()
|
|
|
25
20
|
let remotes: Remotes
|
|
26
21
|
|
|
27
22
|
beforeEach(() => {
|
|
28
|
-
remotes = new Remotes(
|
|
23
|
+
remotes = new Remotes(host)
|
|
29
24
|
})
|
|
30
25
|
|
|
31
26
|
it('should create remote', async () => {
|
|
32
27
|
const remote = await remotes.discover(namespace, name)
|
|
33
28
|
|
|
34
|
-
assert.ok(
|
|
29
|
+
assert.ok(host.remote.mock.calls.some((call: any) => call.arguments.length === 2 && isPartial(call.arguments[0], { namespace, name }) && call.arguments[1] !== null && call.arguments[1] !== undefined))
|
|
35
30
|
|
|
36
|
-
assert.deepStrictEqual(remote, await
|
|
31
|
+
assert.deepStrictEqual(remote, await host.remote.mock.calls[0].result)
|
|
37
32
|
})
|
|
38
33
|
|
|
39
34
|
it('should be instance of Connector', async () => {
|
|
@@ -49,7 +44,7 @@ it('should depend on created remotes', async () => {
|
|
|
49
44
|
it('should attribute calls to the gateway', async () => {
|
|
50
45
|
await remotes.discover(namespace, name)
|
|
51
46
|
|
|
52
|
-
assert.ok(
|
|
47
|
+
assert.ok(host.remote.mock.calls.some((call: any) => call.arguments.length === 2 && call.arguments[0] !== null && call.arguments[0] !== undefined && isDeepStrictEqual(call.arguments[1], { service: 'exposition' })))
|
|
53
48
|
})
|
|
54
49
|
|
|
55
50
|
function isPartial (actual, expected) {
|
package/source/Remotes.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Locator, Connector, type Remote, type Source } from '@toa.io/core'
|
|
2
|
-
import { type
|
|
2
|
+
import { type Host } from './Factory.js'
|
|
3
3
|
|
|
4
4
|
export class Remotes extends Connector {
|
|
5
|
-
private readonly
|
|
5
|
+
private readonly host: Host
|
|
6
6
|
private readonly cache: Record<string, Promise<Remote>> = {}
|
|
7
7
|
|
|
8
|
-
public constructor (
|
|
8
|
+
public constructor (host: Host) {
|
|
9
9
|
super()
|
|
10
|
-
this.
|
|
10
|
+
this.host = host
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
public async discover (namespace: string, name: string, version: string = 'local'): Promise<Remote> {
|
|
@@ -21,7 +21,7 @@ export class Remotes extends Connector {
|
|
|
21
21
|
|
|
22
22
|
private async locate (locator: Locator): Promise<Remote> {
|
|
23
23
|
// the gateway is the origin of every call it forwards
|
|
24
|
-
const remote = await this.
|
|
24
|
+
const remote = await this.host.remote(locator, SOURCE)
|
|
25
25
|
|
|
26
26
|
this.depends(remote)
|
|
27
27
|
|
package/source/const.ts
CHANGED
package/source/deployment.ts
CHANGED
|
@@ -61,6 +61,12 @@ export function deployment (_: unknown, annotation?: Annotation): Dependency {
|
|
|
61
61
|
if (annotation.protocol !== undefined)
|
|
62
62
|
properties.protocol = annotation.protocol
|
|
63
63
|
|
|
64
|
+
if (annotation.ip !== undefined)
|
|
65
|
+
properties.ip = annotation.ip
|
|
66
|
+
|
|
67
|
+
if (annotation.bouncer !== undefined)
|
|
68
|
+
properties.bouncer = annotation.bouncer
|
|
69
|
+
|
|
64
70
|
service.variables!.push({
|
|
65
71
|
name: 'TOA_EXPOSITION_PROPERTIES',
|
|
66
72
|
value: JSON.stringify(properties)
|
|
@@ -77,4 +83,4 @@ export function deployment (_: unknown, annotation?: Annotation): Dependency {
|
|
|
77
83
|
return { services: [service] }
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
type Properties = Pick<Annotation, 'authorities' | 'debug' | 'protocol'>
|
|
86
|
+
type Properties = Pick<Annotation, 'authorities' | 'debug' | 'protocol' | 'bouncer' | 'ip'>
|
|
@@ -25,6 +25,9 @@ export class Assert implements Directive {
|
|
|
25
25
|
throw new http.Unauthorized()
|
|
26
26
|
|
|
27
27
|
if (identity === null) {
|
|
28
|
+
if (!Incept.acceptable(context))
|
|
29
|
+
throw new http.Unauthorized()
|
|
30
|
+
|
|
28
31
|
context.identity = await Incept.incept(context, newid())
|
|
29
32
|
|
|
30
33
|
context.pipelines.response.push((response) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { permits } from './Authorization.js'
|
|
5
|
+
|
|
6
|
+
const permissions = { '/users/me/**': ['GET'], '/notes/': ['*'] }
|
|
7
|
+
|
|
8
|
+
it('should permit a matching method and path', () => {
|
|
9
|
+
assert.equal(permits(permissions, 'GET', '/users/me/posts/'), true)
|
|
10
|
+
assert.equal(permits(permissions, 'DELETE', '/notes/'), true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should refuse another method or path', () => {
|
|
14
|
+
assert.equal(permits(permissions, 'POST', '/users/me/posts/'), false)
|
|
15
|
+
assert.equal(permits(permissions, 'GET', '/admin/'), false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should be matched on the routed path, not on the request URL', () => {
|
|
19
|
+
const raw = '/users/me/%2e%2e/admin/'
|
|
20
|
+
const routed = new URL(raw, 'https://example.com').pathname
|
|
21
|
+
|
|
22
|
+
assert.equal(routed, '/users/admin/')
|
|
23
|
+
assert.equal(permits(permissions, 'GET', routed), false)
|
|
24
|
+
})
|
|
@@ -16,11 +16,15 @@ import { Federation } from './Federation.js'
|
|
|
16
16
|
import { Anyone } from './Anyone.js'
|
|
17
17
|
import { Input, type Declaration } from './Input.js'
|
|
18
18
|
import { split } from './split.js'
|
|
19
|
-
import { PRIMARY,
|
|
19
|
+
import { PRIMARY, provider as providerOf } from './schemes.js'
|
|
20
|
+
import { ATOM_GROUP } from '../../const.js'
|
|
21
|
+
import { Quotas, Sync } from '../io/lib/throttle/index.js'
|
|
22
|
+
import { Keys } from '../io/lib/throttle/Keys.js'
|
|
20
23
|
import type { Output } from '../../io.js'
|
|
21
24
|
import type { Component } from '@toa.io/core'
|
|
22
25
|
import type { Remotes } from '../../Remotes.js'
|
|
23
26
|
import type { Parameter, DirectiveFamily } from '../../RTD/index.js'
|
|
27
|
+
import type { Host } from '../../Factory.js'
|
|
24
28
|
import type {
|
|
25
29
|
AuthenticationResult,
|
|
26
30
|
Ban,
|
|
@@ -43,6 +47,37 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
43
47
|
private tokens: Component | null = null
|
|
44
48
|
private bans: Component | null = null
|
|
45
49
|
|
|
50
|
+
/** Failed authentications per address, reconciled with the other gateways. */
|
|
51
|
+
private meter: Quotas | null = null
|
|
52
|
+
private sync: Sync | null = null
|
|
53
|
+
|
|
54
|
+
public mount (host: Host, options: http.Options): void {
|
|
55
|
+
this.sync?.dispose()
|
|
56
|
+
this.sync = null
|
|
57
|
+
this.meter = null
|
|
58
|
+
|
|
59
|
+
const bouncer = options.bouncer
|
|
60
|
+
|
|
61
|
+
// keyed by address, which is the deployment's to name, so off until it is set
|
|
62
|
+
if (bouncer === undefined)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
this.meter = new Quotas({
|
|
66
|
+
keys: Keys.create([{ method: 'ip', options: BOUNCER }]),
|
|
67
|
+
requests: bouncer.attempts ?? ATTEMPTS,
|
|
68
|
+
interval: (bouncer.interval ?? INTERVAL) * 1000,
|
|
69
|
+
conditional: true, // charged on a rejection, not on a check
|
|
70
|
+
name: METER
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
this.sync = new Sync(host.atom(ATOM_GROUP))
|
|
74
|
+
this.sync.register(this.meter)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public dispose (): void {
|
|
78
|
+
this.sync?.dispose()
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
public create (name: string, value: any, remotes: Remotes): Directive {
|
|
47
82
|
assert.ok(name in constructors,
|
|
48
83
|
`Directive 'auth:${name}' is not implemented`)
|
|
@@ -68,7 +103,7 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
68
103
|
public async preflight (directives: Directive[],
|
|
69
104
|
context: Context,
|
|
70
105
|
parameters: Parameter[]): Promise<Output> {
|
|
71
|
-
context.identity = await this.resolve(context
|
|
106
|
+
context.identity = await this.resolve(context)
|
|
72
107
|
|
|
73
108
|
for (const directive of directives) {
|
|
74
109
|
const allow = await directive.authorize(context.identity, context, parameters)
|
|
@@ -103,8 +138,12 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
103
138
|
if (await this.banned(identity))
|
|
104
139
|
throw new http.Unauthorized()
|
|
105
140
|
|
|
106
|
-
//
|
|
107
|
-
|
|
141
|
+
// a token carries the roles it was issued with, and the refresh is where they are read again;
|
|
142
|
+
// any other scheme has just read them, unless a Role directive already did
|
|
143
|
+
if (identity.scheme === PRIMARY)
|
|
144
|
+
identity.roles = await Role.get(identity, this.discovery.roles)
|
|
145
|
+
else
|
|
146
|
+
identity.roles ??= await Role.get(identity, this.discovery.roles)
|
|
108
147
|
this.tokens ??= await this.discovery.tokens
|
|
109
148
|
|
|
110
149
|
const token = await this.tokens.invoke<string>('encrypt', {
|
|
@@ -118,12 +157,20 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
118
157
|
response.headers.set('cache-control', 'no-store')
|
|
119
158
|
}
|
|
120
159
|
|
|
121
|
-
private async resolve (
|
|
160
|
+
private async resolve (context: Context): Promise<Identity | null> {
|
|
161
|
+
const { authority } = context
|
|
162
|
+
const { authorization } = context.request.headers
|
|
163
|
+
|
|
122
164
|
if (authorization === undefined)
|
|
123
165
|
return null
|
|
124
166
|
|
|
167
|
+
const retry = this.meter?.check(context, NONE) ?? 0
|
|
168
|
+
|
|
169
|
+
if (retry > 0)
|
|
170
|
+
throw new http.TooManyRequests(retry)
|
|
171
|
+
|
|
125
172
|
const [scheme, credentials] = split(authorization)
|
|
126
|
-
const provider =
|
|
173
|
+
const provider = providerOf(scheme)
|
|
127
174
|
|
|
128
175
|
if (provider === undefined)
|
|
129
176
|
throw new http.Unauthorized(`Unknown authentication scheme '${scheme}'`)
|
|
@@ -141,9 +188,14 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
141
188
|
if (result instanceof Error) {
|
|
142
189
|
const code: string | unknown = (result as unknown as { code: string }).code
|
|
143
190
|
|
|
144
|
-
if (typeof code === 'string')
|
|
191
|
+
if (typeof code === 'string') {
|
|
145
192
|
console.info('Authentication failed', { code })
|
|
146
193
|
|
|
194
|
+
context.rejection = code
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.meter?.use(context, null)
|
|
198
|
+
|
|
147
199
|
return null
|
|
148
200
|
}
|
|
149
201
|
|
|
@@ -163,10 +215,8 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
163
215
|
if (permissions === undefined)
|
|
164
216
|
return true
|
|
165
217
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
glob(pattern).match(context.request.url)
|
|
169
|
-
})
|
|
218
|
+
// the route is matched on the normalized path, so the permission is too
|
|
219
|
+
return permits(permissions, context.request.method, context.url.pathname)
|
|
170
220
|
}
|
|
171
221
|
|
|
172
222
|
private async banned (identity: Identity): Promise<boolean> {
|
|
@@ -178,6 +228,13 @@ export class Authorization implements DirectiveFamily<Directive, Extension> {
|
|
|
178
228
|
}
|
|
179
229
|
}
|
|
180
230
|
|
|
231
|
+
/** Whether the permissions admit the method on the path the request is routed by. */
|
|
232
|
+
export function permits (permissions: Record<string, string[]>, method: string, pathname: string): boolean {
|
|
233
|
+
return Object.entries(permissions).some(([pattern, methods]) =>
|
|
234
|
+
methods.some((allowed) => allowed === '*' || allowed === method) &&
|
|
235
|
+
glob(pattern).match(pathname))
|
|
236
|
+
}
|
|
237
|
+
|
|
181
238
|
/**
|
|
182
239
|
* `minimatch(str, pattern)` compiles the pattern on every call, and a permission is
|
|
183
240
|
* matched on every request the identity makes. Patterns arrive with an identity, hence
|
|
@@ -198,6 +255,18 @@ function glob (pattern: string): Minimatch {
|
|
|
198
255
|
return compiled
|
|
199
256
|
}
|
|
200
257
|
|
|
258
|
+
/** What an address may fail at once, and the seconds it takes to earn them back. */
|
|
259
|
+
const ATTEMPTS = 20
|
|
260
|
+
const INTERVAL = 60
|
|
261
|
+
|
|
262
|
+
/** Who is speaking when a request cannot be keyed. */
|
|
263
|
+
const BOUNCER = 'Authentication bouncer'
|
|
264
|
+
|
|
265
|
+
/** The meter's name in the atom: `atom:exposition:meter:credentials:<address>`. */
|
|
266
|
+
const METER = 'credentials'
|
|
267
|
+
|
|
268
|
+
const NONE: Parameter[] = []
|
|
269
|
+
|
|
201
270
|
const GLOBS = new Map<string, Minimatch>()
|
|
202
271
|
const GLOBS_LIMIT = 1024
|
|
203
272
|
|
|
@@ -3,7 +3,7 @@ import { console } from 'openspan'
|
|
|
3
3
|
import * as http from '../../HTTP/index.js'
|
|
4
4
|
import { split } from './split.js'
|
|
5
5
|
import { create } from './create.js'
|
|
6
|
-
import {
|
|
6
|
+
import { INCEPTION, UNKNOWN, provider as providerOf } from './schemes.js'
|
|
7
7
|
import { Role } from './Role.js'
|
|
8
8
|
import type { Component } from '@toa.io/core'
|
|
9
9
|
import type { Maybe } from '@toa.io/types'
|
|
@@ -26,7 +26,7 @@ export class Incept implements Directive {
|
|
|
26
26
|
|
|
27
27
|
public static async incept (context: Context, id: string): Promise<Identity> {
|
|
28
28
|
const [scheme, credentials] = split(context.request.headers.authorization!)
|
|
29
|
-
const provider =
|
|
29
|
+
const provider = providerOf(scheme)
|
|
30
30
|
|
|
31
31
|
if (provider === undefined)
|
|
32
32
|
throw new http.BadRequest('Authentication scheme is not supported')
|
|
@@ -61,8 +61,13 @@ export class Incept implements Directive {
|
|
|
61
61
|
return identity
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
/** Credentials that were rejected for any reason but being unknown are not incepted. */
|
|
65
|
+
public static acceptable (context: Context): boolean {
|
|
66
|
+
return context.request.headers.authorization === undefined || context.rejection === UNKNOWN
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public authorize (identity: Identity | null, context: Context): boolean {
|
|
70
|
+
return identity === null && Incept.acceptable(context)
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
public reply (context: Context): http.OutgoingMessage | null {
|
|
@@ -36,7 +36,7 @@ it('should return false if not matched', async () => {
|
|
|
36
36
|
|
|
37
37
|
assert.ok(remote.invoke.mock.calls.some((call: any) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], 'list') && isDeepStrictEqual(call.arguments[1], {
|
|
38
38
|
query: {
|
|
39
|
-
criteria: `identity
|
|
39
|
+
criteria: `identity=="${identity.id}"`,
|
|
40
40
|
limit: 1024
|
|
41
41
|
}
|
|
42
42
|
})))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
+
import { quote } from '@toa.io/generic'
|
|
2
3
|
import { type Component, type Query } from '@toa.io/core'
|
|
3
4
|
import { type Directive, type Identity } from './types.js'
|
|
4
5
|
import type { Parameter } from '../../RTD/index.js'
|
|
@@ -19,7 +20,7 @@ export class Role implements Directive {
|
|
|
19
20
|
this.remote ??= await discovery
|
|
20
21
|
|
|
21
22
|
const query: Query = {
|
|
22
|
-
criteria: `identity==${identity.id}`,
|
|
23
|
+
criteria: `identity==${quote(identity.id)}`,
|
|
23
24
|
limit: 1024
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { provider } from './schemes.js'
|
|
5
|
+
|
|
6
|
+
it('should resolve a known scheme', () => {
|
|
7
|
+
assert.equal(provider('basic'), 'basic')
|
|
8
|
+
assert.equal(provider('bearer'), 'federation')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should not resolve a property of Object.prototype', () => {
|
|
12
|
+
for (const name of ['constructor', '__proto__', 'toString', 'hasOwnProperty'])
|
|
13
|
+
assert.equal(provider(name), undefined)
|
|
14
|
+
})
|