@toa.io/extensions.exposition 1.0.0-alpha.287 → 1.0.0-alpha.289
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 +19 -0
- package/components/identity.basic/tsconfig.tsbuildinfo +1 -1
- package/components/identity.clients/tsconfig.tsbuildinfo +1 -1
- package/components/identity.credentials/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/tsconfig.tsbuildinfo +1 -1
- package/components/identity.grants/tsconfig.tsbuildinfo +1 -1
- package/components/identity.keys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.otp/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/manifest.toa.yaml +2 -1
- package/components/identity.passkeys/source/errors.test.ts +34 -0
- package/components/identity.passkeys/source/webauthn.test.ts +26 -0
- package/components/identity.passkeys/tsconfig.tsbuildinfo +1 -1
- package/components/identity.passkeys/types/toa.d.ts +1 -1
- package/components/identity.roles/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/tsconfig.tsbuildinfo +1 -1
- package/documentation/access.md +23 -4
- package/documentation/discovery.md +81 -0
- package/documentation/help.md +122 -0
- package/documentation/introspection.md +42 -9
- package/documentation/mcp.md +13 -16
- package/documentation/octets.md +20 -0
- package/features/cors.feature +31 -1
- package/features/dev.feature +2 -0
- package/features/discovery.feature +213 -0
- package/features/discovery.ui.feature +213 -0
- package/features/help.feature +512 -0
- package/features/introspection.feature +94 -8
- package/features/map.feature +1 -0
- package/features/mcp.feature +74 -12
- package/features/methods.feature +2 -1
- package/features/oauth.grants.feature +3 -1
- package/features/octets.download.feature +1 -0
- package/features/octets.feature +59 -0
- package/features/octets.meta.feature +1 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +10 -0
- package/features/steps/Components.ts +4 -2
- package/features/steps/Database.ts +7 -1
- package/features/steps/Gateway.ts +23 -21
- package/features/steps/Parameters.ts +37 -16
- package/features/steps/Realtime.ts +3 -3
- package/package.json +7 -5
- package/readme.md +3 -0
- package/source/Directive.ts +4 -11
- package/source/Discovery/Explorer.ts +34 -0
- package/source/Discovery/Site.test.ts +147 -0
- package/source/Discovery/Site.ts +202 -0
- package/source/Discovery/index.ts +3 -0
- package/source/Discovery/tree.test.ts +175 -0
- package/source/Discovery/tree.ts +77 -0
- package/source/Discovery/trunk.ts +33 -0
- package/source/Endpoint.ts +4 -0
- package/source/Factory.ts +3 -86
- package/source/Gateway.ts +34 -4
- package/source/HTTP/Context.ts +7 -0
- package/source/HTTP/Probe.ts +0 -11
- package/source/HTTP/Server.ts +2 -3
- package/source/HTTP/messages.ts +4 -2
- package/source/Introspection.ts +58 -7
- package/source/MCP/schema.ts +16 -5
- package/source/MCP/tools.ts +14 -32
- package/source/Mapping.ts +5 -0
- package/source/Query.ts +17 -2
- package/source/RTD/Directives.ts +10 -0
- package/source/RTD/Endpoint.ts +8 -1
- package/source/RTD/Node.ts +20 -6
- package/source/RTD/Tree.ts +19 -9
- package/source/RTD/factory.ts +37 -1
- package/source/RTD/segment.ts +21 -0
- package/source/const.ts +26 -0
- package/source/deployment.ts +2 -2
- package/source/directives/auth/Anonymous.test.ts +31 -5
- package/source/directives/auth/Anonymous.ts +15 -3
- package/source/directives/auth/Anyone.ts +6 -0
- package/source/directives/auth/Assert.ts +5 -0
- package/source/directives/auth/Delegate.ts +5 -2
- package/source/directives/auth/Federation.ts +11 -0
- package/source/directives/auth/Id.ts +15 -0
- package/source/directives/auth/Input.ts +5 -0
- package/source/directives/auth/Role.ts +17 -0
- package/source/directives/cors/CORS.test.ts +75 -0
- package/source/directives/cors/CORS.ts +10 -3
- package/source/directives/help/Family.ts +122 -0
- package/source/directives/help/Help.test.ts +227 -0
- package/source/directives/help/Help.ts +47 -0
- package/source/directives/help/Parameters.ts +47 -0
- package/source/directives/help/described.ts +57 -0
- package/source/directives/help/index.ts +45 -0
- package/source/directives/index.ts +8 -1
- package/source/directives/map/Headers.ts +6 -6
- package/source/directives/mcp/MCP.ts +12 -21
- package/source/directives/mcp/Tool.test.ts +22 -68
- package/source/directives/mcp/Tool.ts +14 -41
- package/source/directives/octets/Directive.ts +7 -0
- package/source/directives/octets/Octets.ts +8 -0
- package/source/directives/octets/Put.ts +10 -0
- package/source/manifest.ts +1 -1
- package/source/root.ts +2 -2
- package/source/service.ts +94 -0
- package/source/shortcuts.ts +14 -0
- package/transpiled/Directive.d.ts +1 -1
- package/transpiled/Directive.js +3 -11
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Discovery/Explorer.d.ts +11 -0
- package/transpiled/Discovery/Explorer.js +29 -0
- package/transpiled/Discovery/Explorer.js.map +1 -0
- package/transpiled/Discovery/Site.d.ts +43 -0
- package/transpiled/Discovery/Site.js +171 -0
- package/transpiled/Discovery/Site.js.map +1 -0
- package/transpiled/Discovery/index.d.ts +3 -0
- package/transpiled/Discovery/index.js +4 -0
- package/transpiled/Discovery/index.js.map +1 -0
- package/transpiled/Discovery/tree.d.ts +26 -0
- package/transpiled/Discovery/tree.js +50 -0
- package/transpiled/Discovery/tree.js.map +1 -0
- package/transpiled/Discovery/trunk.d.ts +13 -0
- package/transpiled/Discovery/trunk.js +29 -0
- package/transpiled/Discovery/trunk.js.map +1 -0
- package/transpiled/Endpoint.d.ts +2 -1
- package/transpiled/Endpoint.js +3 -0
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +3 -61
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +4 -0
- package/transpiled/Gateway.js +29 -4
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +6 -0
- package/transpiled/HTTP/Context.js +6 -0
- package/transpiled/HTTP/Context.js.map +1 -1
- package/transpiled/HTTP/Probe.d.ts +0 -10
- package/transpiled/HTTP/Probe.js +0 -10
- package/transpiled/HTTP/Probe.js.map +1 -1
- package/transpiled/HTTP/Server.d.ts +0 -1
- package/transpiled/HTTP/Server.js +2 -2
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/messages.js +4 -2
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Introspection.d.ts +38 -6
- package/transpiled/Introspection.js +18 -1
- package/transpiled/Introspection.js.map +1 -1
- package/transpiled/MCP/schema.d.ts +2 -2
- package/transpiled/MCP/schema.js +11 -5
- package/transpiled/MCP/schema.js.map +1 -1
- package/transpiled/MCP/tools.d.ts +1 -1
- package/transpiled/MCP/tools.js +12 -28
- package/transpiled/MCP/tools.js.map +1 -1
- package/transpiled/Mapping.d.ts +2 -0
- package/transpiled/Mapping.js +4 -0
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/Query.d.ts +12 -0
- package/transpiled/Query.js +16 -2
- package/transpiled/Query.js.map +1 -1
- package/transpiled/RTD/Directives.d.ts +8 -0
- package/transpiled/RTD/Endpoint.d.ts +7 -1
- package/transpiled/RTD/Node.d.ts +11 -2
- package/transpiled/RTD/Node.js +12 -2
- package/transpiled/RTD/Node.js.map +1 -1
- package/transpiled/RTD/Tree.js +15 -8
- package/transpiled/RTD/Tree.js.map +1 -1
- package/transpiled/RTD/factory.js +27 -1
- package/transpiled/RTD/factory.js.map +1 -1
- package/transpiled/RTD/segment.d.ts +7 -0
- package/transpiled/RTD/segment.js +17 -0
- package/transpiled/RTD/segment.js.map +1 -1
- package/transpiled/const.d.ts +21 -0
- package/transpiled/const.js +21 -0
- package/transpiled/const.js.map +1 -1
- package/transpiled/deployment.js +2 -2
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.d.ts +11 -2
- package/transpiled/directives/auth/Anonymous.js +13 -3
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Anyone.d.ts +3 -0
- package/transpiled/directives/auth/Anyone.js +4 -0
- package/transpiled/directives/auth/Anyone.js.map +1 -1
- package/transpiled/directives/auth/Assert.d.ts +2 -0
- package/transpiled/directives/auth/Assert.js +4 -0
- package/transpiled/directives/auth/Assert.js.map +1 -1
- package/transpiled/directives/auth/Delegate.d.ts +4 -1
- package/transpiled/directives/auth/Delegate.js +5 -2
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Federation.d.ts +5 -0
- package/transpiled/directives/auth/Federation.js +8 -0
- package/transpiled/directives/auth/Federation.js.map +1 -1
- package/transpiled/directives/auth/Id.d.ts +9 -0
- package/transpiled/directives/auth/Id.js +12 -0
- package/transpiled/directives/auth/Id.js.map +1 -1
- package/transpiled/directives/auth/Input.d.ts +2 -0
- package/transpiled/directives/auth/Input.js +4 -0
- package/transpiled/directives/auth/Input.js.map +1 -1
- package/transpiled/directives/auth/Role.d.ts +7 -0
- package/transpiled/directives/auth/Role.js +11 -0
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/cors/CORS.js +9 -2
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/help/Family.d.ts +28 -0
- package/transpiled/directives/help/Family.js +83 -0
- package/transpiled/directives/help/Family.js.map +1 -0
- package/transpiled/directives/help/Help.d.ts +22 -0
- package/transpiled/directives/help/Help.js +36 -0
- package/transpiled/directives/help/Help.js.map +1 -0
- package/transpiled/directives/help/Parameters.d.ts +18 -0
- package/transpiled/directives/help/Parameters.js +34 -0
- package/transpiled/directives/help/Parameters.js.map +1 -0
- package/transpiled/directives/help/described.d.ts +16 -0
- package/transpiled/directives/help/described.js +23 -0
- package/transpiled/directives/help/described.js.map +1 -0
- package/transpiled/directives/help/index.d.ts +22 -0
- package/transpiled/directives/help/index.js +34 -0
- package/transpiled/directives/help/index.js.map +1 -0
- package/transpiled/directives/index.d.ts +4 -0
- package/transpiled/directives/index.js +8 -1
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/map/Headers.d.ts +5 -0
- package/transpiled/directives/map/Headers.js +7 -5
- package/transpiled/directives/map/Headers.js.map +1 -1
- package/transpiled/directives/mcp/MCP.d.ts +8 -5
- package/transpiled/directives/mcp/MCP.js +9 -13
- package/transpiled/directives/mcp/MCP.js.map +1 -1
- package/transpiled/directives/mcp/Tool.d.ts +6 -12
- package/transpiled/directives/mcp/Tool.js +10 -23
- package/transpiled/directives/mcp/Tool.js.map +1 -1
- package/transpiled/directives/octets/Directive.d.ts +6 -0
- package/transpiled/directives/octets/Directive.js.map +1 -1
- package/transpiled/directives/octets/Octets.d.ts +3 -0
- package/transpiled/directives/octets/Octets.js +6 -0
- package/transpiled/directives/octets/Octets.js.map +1 -1
- package/transpiled/directives/octets/Put.d.ts +3 -0
- package/transpiled/directives/octets/Put.js +8 -0
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/manifest.js +1 -1
- package/transpiled/root.js +2 -2
- package/transpiled/root.js.map +1 -1
- package/transpiled/service.d.ts +7 -0
- package/transpiled/service.js +74 -0
- package/transpiled/service.js.map +1 -0
- package/transpiled/shortcuts.d.ts +3 -0
- package/transpiled/shortcuts.js +13 -0
- package/transpiled/shortcuts.js.map +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/ui/dist/_app/env.js +1 -0
- package/ui/dist/_app/immutable/assets/0.Bl7vLmIK.css +2 -0
- package/ui/dist/_app/immutable/assets/2.BC0-Jhqu.css +1 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/ui/dist/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/ui/dist/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/ui/dist/_app/immutable/chunks/CDMRPWCx.js +6 -0
- package/ui/dist/_app/immutable/chunks/DJTKIUs8.js +1 -0
- package/ui/dist/_app/immutable/chunks/DjIYL2zC.js +3 -0
- package/ui/dist/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/ui/dist/_app/immutable/chunks/z-DVUbQC.js +1 -0
- package/ui/dist/_app/immutable/entry/app.5S2Jtv46.js +2 -0
- package/ui/dist/_app/immutable/entry/start.Bqi6IBmH.js +1 -0
- package/ui/dist/_app/immutable/nodes/0.D4LwNilH.js +1 -0
- package/ui/dist/_app/immutable/nodes/1.CSRya9So.js +1 -0
- package/ui/dist/_app/immutable/nodes/2.BtiGmR6I.js +14 -0
- package/ui/dist/_app/version.json +1 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/favicon-96x96.png +0 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/index.html +66 -0
- package/components/identity.bans/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.basic/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.clients/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.federation/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.grants/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.keys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.otp/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.passkeys/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.roles/migrations/0001-system-properties.yaml +0 -14
- package/components/identity.tokens/migrations/0001-system-properties.yaml +0 -14
package/source/RTD/Directives.ts
CHANGED
|
@@ -29,6 +29,9 @@ export interface Directives {
|
|
|
29
29
|
export interface DirectiveFactory {
|
|
30
30
|
create: (directives: syntax.Directive[], route: string) => Directives
|
|
31
31
|
|
|
32
|
+
/** Whether this declaration is carried into the nodes below the one it is on. */
|
|
33
|
+
inheritable: (directive: syntax.Directive) => boolean
|
|
34
|
+
|
|
32
35
|
/** Runs every family's `preflight`, which is request-scoped and needs no node. */
|
|
33
36
|
preflight: (context: Context) => Promise<void>
|
|
34
37
|
|
|
@@ -50,6 +53,13 @@ export interface DirectiveFamily<TDirective = any, TExtension = any> {
|
|
|
50
53
|
readonly name: string
|
|
51
54
|
readonly mandatory: boolean
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Whether a declaration applies to everything below the node it is on, as every directive
|
|
58
|
+
* does unless it says otherwise. A family that describes what one node *is* sets this
|
|
59
|
+
* `false`: inherited, it would say the same thing of every resource under it.
|
|
60
|
+
*/
|
|
61
|
+
readonly inherited?: boolean
|
|
62
|
+
|
|
53
63
|
create: (name: string, ...rest: any[]) => TDirective
|
|
54
64
|
|
|
55
65
|
/**
|
package/source/RTD/Endpoint.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Context } from './Context.js'
|
|
|
2
2
|
import type * as http from '../HTTP/index.js'
|
|
3
3
|
import type * as syntax from './syntax/index.js'
|
|
4
4
|
import type * as RTD from './index.js'
|
|
5
|
-
import type { Introspection } from '../Introspection.js'
|
|
5
|
+
import type { Introspection, Schema } from '../Introspection.js'
|
|
6
6
|
|
|
7
7
|
export interface Endpoint {
|
|
8
8
|
call: (
|
|
@@ -12,6 +12,13 @@ export interface Endpoint {
|
|
|
12
12
|
|
|
13
13
|
explain: (parameters: RTD.Parameter[]) => Promise<Introspection>
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* What picks the records a call is about — `criteria`, `sort` and the rest. Not part of
|
|
17
|
+
* what the resource says about itself: it is the same of every queryable one. A
|
|
18
|
+
* procedure carries it, because there it is something the caller sends.
|
|
19
|
+
*/
|
|
20
|
+
selection: () => Record<string, Schema> | null
|
|
21
|
+
|
|
15
22
|
close: () => Promise<void>
|
|
16
23
|
}
|
|
17
24
|
|
package/source/RTD/Node.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Method, type Methods } from './Method.js'
|
|
|
3
3
|
import { type Match, type Parameter } from './Match.js'
|
|
4
4
|
import type { Segment } from './segment.js'
|
|
5
5
|
import type { Context } from '../HTTP/index.js'
|
|
6
|
+
import { guarded, resource, type Described } from '../directives/help/index.js'
|
|
6
7
|
import type { Introspection } from '../Introspection.js'
|
|
7
8
|
|
|
8
9
|
export class Node {
|
|
@@ -72,11 +73,11 @@ export class Node {
|
|
|
72
73
|
for (const route of this.routes) yield* route.walk(segments)
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
): Promise<
|
|
76
|
+
/**
|
|
77
|
+
* What this resource is, and every method of it this caller may reach, in the order they
|
|
78
|
+
* were declared. The two are answered apart because only the methods are `Allow`.
|
|
79
|
+
*/
|
|
80
|
+
public async explain(context: Context, parameters: Parameter[]): Promise<Explained> {
|
|
80
81
|
const entries = Object.entries(this.methods)
|
|
81
82
|
|
|
82
83
|
const explained = await Promise.all(
|
|
@@ -88,7 +89,14 @@ export class Node {
|
|
|
88
89
|
for (let i = 0; i < entries.length; i++)
|
|
89
90
|
if (explained[i] !== null) methods[entries[i][0]] = explained[i]!
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
// every method of a node carries the same declaration, so the first that is there says it
|
|
93
|
+
const stated = entries.length === 0 ? null : resource(entries[0][1].directives)
|
|
94
|
+
const described = { ...stated, ...guarded(methods) }
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
described: Object.keys(described).length === 0 ? null : described,
|
|
98
|
+
methods
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
private replace(node: Node): Node[] {
|
|
@@ -141,6 +149,12 @@ export class Node {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
/** What a resource says of itself, and what it serves. */
|
|
153
|
+
export interface Explained {
|
|
154
|
+
described: Described | null
|
|
155
|
+
methods: Record<string, Introspection>
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
/** A method, and the route template it is reached by. */
|
|
145
159
|
export interface Mount {
|
|
146
160
|
segments: Segment[]
|
package/source/RTD/Tree.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { console } from 'openspan'
|
|
2
2
|
import { refusal, template } from '../RPC/names.js'
|
|
3
|
+
import { DISCOVERY } from '../const.js'
|
|
3
4
|
import { branchTTL, createNode } from './factory.js'
|
|
4
5
|
import { fragment } from './segment.js'
|
|
5
6
|
import type { Mount, Node } from './Node.js'
|
|
@@ -25,7 +26,7 @@ export class Tree {
|
|
|
25
26
|
this.root = node
|
|
26
27
|
this.trunk = this.createNode(node, PROTECTED)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
announce(this.trunk)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
public match(path: string): Match | null {
|
|
@@ -48,7 +49,7 @@ export class Tree {
|
|
|
48
49
|
public merge(node: syntax.Node, extension: unknown): Node[] {
|
|
49
50
|
const branch = this.createNode(node, !PROTECTED, extension)
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
announce(branch)
|
|
52
53
|
|
|
53
54
|
return this.trunk.merge(branch)
|
|
54
55
|
}
|
|
@@ -87,21 +88,30 @@ export class Tree {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
* What is served but cannot be
|
|
91
|
-
*
|
|
91
|
+
* What is served but cannot be reached as it reads. Said once per route as it is built,
|
|
92
|
+
* because either of these is otherwise noticed only by the caller who cannot find it.
|
|
92
93
|
*/
|
|
93
|
-
function
|
|
94
|
+
function announce(node: Node): void {
|
|
94
95
|
const said = new Set<string>()
|
|
95
96
|
|
|
96
97
|
for (const { segments } of node.walk([], TRUNK)) {
|
|
97
|
-
const segment = refusal(segments)
|
|
98
|
-
|
|
99
|
-
if (segment === null) continue
|
|
100
|
-
|
|
101
98
|
const route = template(segments)
|
|
102
99
|
|
|
103
100
|
if (said.has(route)) continue
|
|
104
101
|
|
|
102
|
+
// the page is served under this prefix, before a request is routed at all
|
|
103
|
+
if (route === DISCOVERY || route.startsWith(DISCOVERY + '/')) {
|
|
104
|
+
said.add(route)
|
|
105
|
+
|
|
106
|
+
console.warn('Route is shadowed by the discovery endpoint', { route })
|
|
107
|
+
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const segment = refusal(segments)
|
|
112
|
+
|
|
113
|
+
if (segment === null) continue
|
|
114
|
+
|
|
105
115
|
said.add(route)
|
|
106
116
|
|
|
107
117
|
console.warn('Route cannot be addressed as a procedure', { route, segment })
|
package/source/RTD/factory.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
1
2
|
import { BRANCH_TTL } from '../const.js'
|
|
2
3
|
import { Node, type Properties } from './Node.js'
|
|
3
4
|
import { Route } from './Route.js'
|
|
4
|
-
import { segment } from './segment.js'
|
|
5
|
+
import { fragment, segment } from './segment.js'
|
|
5
6
|
import { Method, type Methods } from './Method.js'
|
|
6
7
|
import type { Context } from './Context.js'
|
|
7
8
|
import type * as syntax from './syntax/index.js'
|
|
@@ -10,6 +11,8 @@ export function createNode(node: syntax.Node, context: Context): Node {
|
|
|
10
11
|
if (node.isolated === true) context.directives.stack = node.directives
|
|
11
12
|
else context.directives.stack = node.directives.concat(context.directives.stack)
|
|
12
13
|
|
|
14
|
+
described(node, context)
|
|
15
|
+
|
|
13
16
|
const routes: Route[] = node.routes.map((route) => createRoute(route, context))
|
|
14
17
|
const methods: Methods = {}
|
|
15
18
|
|
|
@@ -43,6 +46,16 @@ function createRoute(route: syntax.Route, context: Context): Route {
|
|
|
43
46
|
|
|
44
47
|
context.path = join(path, route.path)
|
|
45
48
|
|
|
49
|
+
/*
|
|
50
|
+
* What a node says of itself is not said of what is under it. Its own `/` is the
|
|
51
|
+
* exception: an intermediate node is never what a path matches, because that route
|
|
52
|
+
* answers in its place — so the two are one resource and what describes it carries.
|
|
53
|
+
*/
|
|
54
|
+
if (route.path !== ROOT)
|
|
55
|
+
context.directives.stack = stack.filter((directive) =>
|
|
56
|
+
context.directives.factory.inheritable(directive)
|
|
57
|
+
)
|
|
58
|
+
|
|
46
59
|
const node = createNode(route.node, context)
|
|
47
60
|
|
|
48
61
|
context.directives.stack = stack // restore
|
|
@@ -67,3 +80,26 @@ function createMethod(method: syntax.Method, context: Context): Method {
|
|
|
67
80
|
|
|
68
81
|
return new Method(endpoint, directives)
|
|
69
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A resource is what it is described beside: a node with no methods of its own is never
|
|
86
|
+
* what a path answers, so nothing would carry what it says about itself. Refused where the
|
|
87
|
+
* declaration is, rather than left to be noticed by whoever cannot find it in the answer.
|
|
88
|
+
*/
|
|
89
|
+
function described(node: syntax.Node, context: Context): void {
|
|
90
|
+
const help = node.directives.find(
|
|
91
|
+
(directive) => directive.family === HELP && directive.name === 'node'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (help === undefined || node.methods.length > 0) return
|
|
95
|
+
|
|
96
|
+
// an intermediate node's `/` answers in its place, and carries what it says
|
|
97
|
+
if (node.routes.some((route) => route.path === ROOT)) return
|
|
98
|
+
|
|
99
|
+
const route = '/' + fragment(context.path).join('/')
|
|
100
|
+
|
|
101
|
+
assert.fail(`Directive help:node: '${route}' serves no methods`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ROOT = '/'
|
|
105
|
+
const HELP = 'help'
|
package/source/RTD/segment.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Parameter } from './Match.js'
|
|
2
|
+
|
|
1
3
|
export function segment(path: string): Segment[] {
|
|
2
4
|
return fragment(path).map(parse)
|
|
3
5
|
}
|
|
@@ -28,3 +30,22 @@ export type Segment =
|
|
|
28
30
|
placeholder: string | null
|
|
29
31
|
wildcard?: boolean
|
|
30
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What a route template takes, by name. Describing has no values for them — a template is
|
|
36
|
+
* not a path — and nothing that describes a method reads one. A `*` is skipped: it stands
|
|
37
|
+
* for a segment the caller cannot name, so there is nothing to substitute.
|
|
38
|
+
*/
|
|
39
|
+
export function variables(segments: Segment[]): Parameter[] {
|
|
40
|
+
const params: Parameter[] = []
|
|
41
|
+
|
|
42
|
+
for (const segment of segments) {
|
|
43
|
+
if (segment.fragment !== null) continue
|
|
44
|
+
|
|
45
|
+
if (segment.wildcard === true) params.push({ name: '**', value: '' })
|
|
46
|
+
else if (segment.placeholder !== null)
|
|
47
|
+
params.push({ name: segment.placeholder, value: '' })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return params
|
|
51
|
+
}
|
package/source/const.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export const BRANCH_TTL = 1_800_000
|
|
2
2
|
|
|
3
|
+
/** The broadcast the gateway and its tenants share. */
|
|
4
|
+
export const CHANNEL = 'exposition'
|
|
5
|
+
|
|
3
6
|
/** The replicas of the gateway decide together, whatever context they serve. */
|
|
4
7
|
export const ATOM_GROUP = 'exposition'
|
|
5
8
|
|
|
@@ -17,3 +20,26 @@ export const BATCH = 32
|
|
|
17
20
|
* application could choose is a path it could collide with a route of its own.
|
|
18
21
|
*/
|
|
19
22
|
export const MCP = '/.mcp'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where the resource tree and the page that reads it are served. Pinned for the reason
|
|
26
|
+
* `/.rpc` is: a path an application could choose is a path it could collide with a route of
|
|
27
|
+
* its own. Unlike those two it is not annotated — every entry it carries is what `OPTIONS`
|
|
28
|
+
* on that path already answers to the same caller, and what it adds is the enumeration.
|
|
29
|
+
*/
|
|
30
|
+
export const DISCOVERY = '/.discovery'
|
|
31
|
+
|
|
32
|
+
/** Where the gateway serves. */
|
|
33
|
+
export const PORT = 8000
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reserved for the readiness probe. `8001` is the Telemetry readiness probe's, and `toa export`
|
|
37
|
+
* refuses a port claimed twice — `toa mono` and a local run put every service in one process.
|
|
38
|
+
*/
|
|
39
|
+
export const PROBE = 8004
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The initial delay of the readiness probe. The server does not sleep for it: whoever
|
|
43
|
+
* probes is the one that waits, and doing it here as well only delayed the process twice.
|
|
44
|
+
*/
|
|
45
|
+
export const DELAY = 3 // seconds
|
package/source/deployment.ts
CHANGED
|
@@ -3,10 +3,10 @@ import assert from 'node:assert'
|
|
|
3
3
|
import { type Dependency, type Service } from '@toa.io/operations'
|
|
4
4
|
import { type Annotation } from './Annotation.js'
|
|
5
5
|
import * as schemas from './schemas.js'
|
|
6
|
-
import { shortcuts } from './
|
|
6
|
+
import { shortcuts } from './shortcuts.js'
|
|
7
7
|
import { components } from './Composition.js'
|
|
8
8
|
import { parse } from './RTD/syntax/index.js'
|
|
9
|
-
import { DELAY, PORT, PROBE } from './
|
|
9
|
+
import { DELAY, PORT, PROBE } from './const.js'
|
|
10
10
|
|
|
11
11
|
/** Where Toa's release publishes this service's image. An application takes it
|
|
12
12
|
* instead of building one when its context says `registry.services: published`. */
|
|
@@ -4,8 +4,16 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { Anonymous } from './Anonymous.js'
|
|
5
5
|
import type { Context } from './types.js'
|
|
6
6
|
|
|
7
|
-
const context = (
|
|
8
|
-
|
|
7
|
+
const context = (
|
|
8
|
+
headers: Record<string, string>,
|
|
9
|
+
flags: { procedural?: boolean; exploratory?: boolean } = {}
|
|
10
|
+
): Context =>
|
|
11
|
+
({
|
|
12
|
+
request: { headers },
|
|
13
|
+
procedural: false,
|
|
14
|
+
exploratory: false,
|
|
15
|
+
...flags
|
|
16
|
+
}) as unknown as Context
|
|
9
17
|
|
|
10
18
|
describe('anonymous', () => {
|
|
11
19
|
it('should admit a request that presents nothing', () => {
|
|
@@ -22,14 +30,28 @@ describe('anonymous', () => {
|
|
|
22
30
|
|
|
23
31
|
it('should admit a procedure whatever the request presented', () => {
|
|
24
32
|
// what a procedure answers is a value in an envelope, and the envelope is `no-store`
|
|
25
|
-
const procedural = context({ authorization: 'Token x' }, true)
|
|
33
|
+
const procedural = context({ authorization: 'Token x' }, { procedural: true })
|
|
26
34
|
|
|
27
35
|
assert.equal(new Anonymous(true).authorize(null, procedural), true)
|
|
28
36
|
})
|
|
29
37
|
|
|
38
|
+
it('should admit a description whatever the request presented', () => {
|
|
39
|
+
// a description is not the reply a cache would hold, and varies by who asked in any case
|
|
40
|
+
const exploratory = context({ authorization: 'Token x' }, { exploratory: true })
|
|
41
|
+
|
|
42
|
+
assert.equal(new Anonymous(true).authorize(null, exploratory), true)
|
|
43
|
+
})
|
|
44
|
+
|
|
30
45
|
it('should refuse where it admits nobody', () => {
|
|
31
46
|
assert.equal(new Anonymous(false).authorize(null, context({})), false)
|
|
32
|
-
assert.equal(
|
|
47
|
+
assert.equal(
|
|
48
|
+
new Anonymous(false).authorize(null, context({}, { procedural: true })),
|
|
49
|
+
false
|
|
50
|
+
)
|
|
51
|
+
assert.equal(
|
|
52
|
+
new Anonymous(false).authorize(null, context({}, { exploratory: true })),
|
|
53
|
+
false
|
|
54
|
+
)
|
|
33
55
|
})
|
|
34
56
|
|
|
35
57
|
it('should describe a method as it authorizes one', () => {
|
|
@@ -37,7 +59,11 @@ describe('anonymous', () => {
|
|
|
37
59
|
|
|
38
60
|
assert.equal(directive.admits(null, context({ authorization: 'Token x' })), false)
|
|
39
61
|
assert.equal(
|
|
40
|
-
directive.admits(null, context({ authorization: 'Token x' }, true)),
|
|
62
|
+
directive.admits(null, context({ authorization: 'Token x' }, { procedural: true })),
|
|
63
|
+
true
|
|
64
|
+
)
|
|
65
|
+
assert.equal(
|
|
66
|
+
directive.admits(null, context({ authorization: 'Token x' }, { exploratory: true })),
|
|
41
67
|
true
|
|
42
68
|
)
|
|
43
69
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Directive, type Context } from './types.js'
|
|
2
|
+
import type { Introspection } from '../../Introspection.js'
|
|
2
3
|
|
|
3
4
|
export class Anonymous implements Directive {
|
|
4
5
|
private readonly allow: boolean
|
|
@@ -9,11 +10,13 @@ export class Anonymous implements Directive {
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A credential refuses, because it would make the reply uncacheable — which is the whole
|
|
12
|
-
* of the rule, and none of it applies to a procedure
|
|
13
|
-
* in an envelope, and the envelope is the one thing that is
|
|
13
|
+
* of the rule, and none of it applies to a procedure, nor to a description. What a
|
|
14
|
+
* procedure answers is a value in an envelope, and the envelope is the one thing that is
|
|
15
|
+
* cached or not; what a description answers is what the resource is, which is not the
|
|
16
|
+
* reply a cache would hold and varies by who asked in any case.
|
|
14
17
|
*/
|
|
15
18
|
public authorize(_: any, context: Context): boolean {
|
|
16
|
-
if (context.procedural) return this.allow
|
|
19
|
+
if (context.procedural || context.exploratory) return this.allow
|
|
17
20
|
|
|
18
21
|
return 'authorization' in context.request.headers ? false : this.allow
|
|
19
22
|
}
|
|
@@ -21,4 +24,13 @@ export class Anonymous implements Directive {
|
|
|
21
24
|
public admits(_: any, context: Context): boolean {
|
|
22
25
|
return this.authorize(_, context)
|
|
23
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* That nothing guards it is something said rather than something left unsaid: it is also
|
|
30
|
+
* what refuses a caller presenting a credential, and a client with one has to know which
|
|
31
|
+
* methods not to present it to.
|
|
32
|
+
*/
|
|
33
|
+
public describe(introspection: Introspection): Introspection {
|
|
34
|
+
return this.allow ? { ...introspection, anonymous: true } : introspection
|
|
35
|
+
}
|
|
24
36
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Directive, type Context } from './types.js'
|
|
2
|
+
import type { Introspection } from '../../Introspection.js'
|
|
2
3
|
|
|
3
4
|
export class Anyone implements Directive {
|
|
4
5
|
private readonly allow: boolean
|
|
@@ -14,4 +15,9 @@ export class Anyone implements Directive {
|
|
|
14
15
|
public admits(_: any, context: Context): boolean {
|
|
15
16
|
return this.authorize(_, context)
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
/** Whoever asks, as long as they are somebody. */
|
|
20
|
+
public describe(introspection: Introspection): Introspection {
|
|
21
|
+
return this.allow ? { ...introspection, authenticated: true } : introspection
|
|
22
|
+
}
|
|
17
23
|
}
|
|
@@ -16,6 +16,11 @@ export class Assert implements Directive {
|
|
|
16
16
|
this.disabled = !enabled
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** It admits nobody: it is there to require a credential, not to authorize one. */
|
|
20
|
+
public admits(): boolean {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
public async authorize(identity: Identity | null, context: Context): Promise<boolean> {
|
|
20
25
|
if (!this.disabled) await this.incept(context, identity)
|
|
21
26
|
|
|
@@ -28,11 +28,14 @@ export class Delegate implements Directive {
|
|
|
28
28
|
return identity !== null
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* It takes an identity, and the property it embeds is that identity's — so a caller has
|
|
33
|
+
* nothing to put there.
|
|
34
|
+
*/
|
|
32
35
|
public describe(introspection: Introspection): Introspection {
|
|
33
36
|
take(introspection, this.property)
|
|
34
37
|
|
|
35
|
-
return introspection
|
|
38
|
+
return { ...introspection, authenticated: true }
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
private embed(body: unknown, identity: Identity): Record<string, unknown> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import type { Directive, Identity, Context } from './types.js'
|
|
3
|
+
import type { Introspection } from '../../Introspection.js'
|
|
3
4
|
import type { Parameter } from '../../RTD/index.js'
|
|
4
5
|
|
|
5
6
|
export class Federation implements Directive {
|
|
@@ -16,6 +17,16 @@ export class Federation implements Directive {
|
|
|
16
17
|
)
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/** Which claims it takes needs the request; that it takes an identity does not. */
|
|
21
|
+
public admits(identity: Identity | null): boolean | undefined {
|
|
22
|
+
return identity === null ? false : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Which claims is the application's business; that it takes an identity is the caller's. */
|
|
26
|
+
public describe(introspection: Introspection): Introspection {
|
|
27
|
+
return { ...introspection, authenticated: true }
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
public authorize(
|
|
20
31
|
identity: Identity | null,
|
|
21
32
|
context: Context,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Parameter } from '../../RTD/index.js'
|
|
2
2
|
import { type Directive, type Identity } from './types.js'
|
|
3
|
+
import type { Introspection } from '../../Introspection.js'
|
|
3
4
|
|
|
4
5
|
export class Id implements Directive {
|
|
5
6
|
private readonly parameter: string
|
|
@@ -8,6 +9,20 @@ export class Id implements Directive {
|
|
|
8
9
|
this.parameter = parameter
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Whether it is *this* identity's cannot be told from a description, which has no route
|
|
14
|
+
* variable to read — but that there must be an identity at all can, and a caller with
|
|
15
|
+
* none is refused whatever the value would have been.
|
|
16
|
+
*/
|
|
17
|
+
public admits(identity: Identity | null): boolean | undefined {
|
|
18
|
+
return identity === null ? false : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Reaching it is being the identity it is about, which is what `private` says. */
|
|
22
|
+
public describe(introspection: Introspection): Introspection {
|
|
23
|
+
return { ...introspection, private: true }
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
public authorize(
|
|
12
27
|
identity: Identity | null,
|
|
13
28
|
_: unknown,
|
|
@@ -12,6 +12,11 @@ export class Input implements Directive {
|
|
|
12
12
|
)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** It admits nobody: it says what a body may carry, not who may send one. */
|
|
16
|
+
public admits(): boolean {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
public async authorize(
|
|
16
21
|
identity: Identity | null,
|
|
17
22
|
context: Context,
|
|
@@ -4,6 +4,7 @@ import { type Component } from '@toa.io/core'
|
|
|
4
4
|
import type { Query } from '@toa.io/core/types'
|
|
5
5
|
import { type Directive, type Identity } from './types.js'
|
|
6
6
|
import type { Parameter } from '../../RTD/index.js'
|
|
7
|
+
import type { Introspection } from '../../Introspection.js'
|
|
7
8
|
|
|
8
9
|
export class Role implements Directive {
|
|
9
10
|
public static remote: Component | null = null
|
|
@@ -31,6 +32,19 @@ export class Role implements Directive {
|
|
|
31
32
|
return await this.remote.invoke('list', { query })
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Reaching it takes a role, which is what `protected` says. A role of the `system` scope
|
|
37
|
+
* says so besides: what it guards is the machinery an application runs on rather than
|
|
38
|
+
* anything it serves, and a reader has no business being offered it as one.
|
|
39
|
+
*/
|
|
40
|
+
public describe(introspection: Introspection): Introspection {
|
|
41
|
+
const system = this.roles.some(
|
|
42
|
+
(role) => role === SYSTEM || role.startsWith(SYSTEM + ':')
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return { ...introspection, protected: true, ...(system ? { system: true } : {}) }
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
public async authorize(
|
|
35
49
|
identity: Identity | null,
|
|
36
50
|
_: unknown,
|
|
@@ -80,3 +94,6 @@ export class Role implements Directive {
|
|
|
80
94
|
)
|
|
81
95
|
}
|
|
82
96
|
}
|
|
97
|
+
|
|
98
|
+
/** The scope of what an application runs on, as opposed to what it serves. */
|
|
99
|
+
const SYSTEM = 'system'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { CORS } from './CORS.js'
|
|
5
|
+
import type { Input } from '../../io.js'
|
|
6
|
+
|
|
7
|
+
const input = (method: string, headers: Record<string, string>): Input =>
|
|
8
|
+
({
|
|
9
|
+
request: { method, headers },
|
|
10
|
+
pipelines: { body: [], response: [] }
|
|
11
|
+
}) as unknown as Input
|
|
12
|
+
|
|
13
|
+
describe('cors', () => {
|
|
14
|
+
let cors: CORS
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
cors = new CORS()
|
|
18
|
+
cors.reset()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should answer a preflight', () => {
|
|
22
|
+
const output = cors.intercept(
|
|
23
|
+
input('OPTIONS', {
|
|
24
|
+
origin: 'https://hello.world',
|
|
25
|
+
'access-control-request-method': 'GET'
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert.equal(output?.status, 204)
|
|
30
|
+
assert.equal(output?.headers?.get('access-control-allow-origin'), 'https://hello.world')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should allow OPTIONS, so that one can be preflighted at all', () => {
|
|
34
|
+
const output = cors.intercept(
|
|
35
|
+
input('OPTIONS', {
|
|
36
|
+
origin: 'https://hello.world',
|
|
37
|
+
'access-control-request-method': 'OPTIONS'
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert.match(output?.headers?.get('access-control-allow-methods') ?? '', /\bOPTIONS\b/)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should pass an OPTIONS that is not a preflight through', () => {
|
|
45
|
+
/*
|
|
46
|
+
* A browser puts `Origin` on every request whose method is not `GET` or `HEAD`, its own
|
|
47
|
+
* `OPTIONS` included — so answering this one here would put introspection out of reach
|
|
48
|
+
* of any page. Do not restore the `Origin`-only test.
|
|
49
|
+
*/
|
|
50
|
+
const output = cors.intercept(input('OPTIONS', { origin: 'https://hello.world' }))
|
|
51
|
+
|
|
52
|
+
assert.equal(output, null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should decorate a reply that is not a preflight', () => {
|
|
56
|
+
const request = input('OPTIONS', { origin: 'https://hello.world' })
|
|
57
|
+
|
|
58
|
+
cors.intercept(request)
|
|
59
|
+
|
|
60
|
+
const message = {}
|
|
61
|
+
|
|
62
|
+
for (const transform of request.pipelines.response) transform(message)
|
|
63
|
+
|
|
64
|
+
const headers = (message as { headers?: Headers }).headers
|
|
65
|
+
|
|
66
|
+
assert.equal(headers?.get('access-control-allow-origin'), 'https://hello.world')
|
|
67
|
+
assert.equal(headers?.get('access-control-allow-credentials'), 'true')
|
|
68
|
+
assert.equal(headers?.get('vary'), 'origin')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should pass a request carrying no origin through', () => {
|
|
72
|
+
assert.equal(cors.intercept(input('OPTIONS', {})), null)
|
|
73
|
+
assert.equal(cors.intercept(input('GET', {})), null)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -16,7 +16,7 @@ export class CORS implements Interceptor {
|
|
|
16
16
|
private requestHeaders = new Set<string>(REQUEST_HEADERS)
|
|
17
17
|
|
|
18
18
|
private readonly headers = new Headers({
|
|
19
|
-
'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK',
|
|
19
|
+
'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK, OPTIONS',
|
|
20
20
|
'access-control-allow-credentials': 'true',
|
|
21
21
|
'access-control-allow-headers': this.allowedHeaders(),
|
|
22
22
|
'access-control-max-age': '3600',
|
|
@@ -26,8 +26,15 @@ export class CORS implements Interceptor {
|
|
|
26
26
|
|
|
27
27
|
public intercept(input: Input): Output {
|
|
28
28
|
const origin = input.request.headers.origin
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const requested = input.request.headers['access-control-request-method']
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* A preflight is `OPTIONS` carrying `Access-Control-Request-Method`, and only that. A
|
|
33
|
+
* browser puts `Origin` on every request whose method is not `GET` or `HEAD` — its own
|
|
34
|
+
* `OPTIONS` included, same-origin or not — so answering that one here would put
|
|
35
|
+
* introspection out of reach of any page.
|
|
36
|
+
*/
|
|
37
|
+
if (origin !== undefined && requested !== undefined && input.request.method === 'OPTIONS')
|
|
31
38
|
return this.preflightResponse(origin)
|
|
32
39
|
|
|
33
40
|
input.pipelines.response.push((output) => {
|