@zhin.js/middleware 1.1.16 → 1.1.17
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/README.md +1 -1
- package/lib/definition.d.ts +8 -0
- package/lib/definition.js +11 -1
- package/lib/middleware-index.d.ts +2 -0
- package/lib/middleware-index.js +26 -0
- package/package.json +5 -5
- package/src/continuation.ts +0 -7
- package/src/definition.ts +0 -181
- package/src/index.ts +0 -11
- package/src/middleware-index.ts +0 -251
- package/src/provider.ts +0 -30
package/README.md
CHANGED
package/lib/definition.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface MiddlewareDefinitionBase<TAdapter extends string | undefined =
|
|
|
31
31
|
readonly phase: MiddlewarePhase;
|
|
32
32
|
readonly order: number;
|
|
33
33
|
readonly adapter?: TAdapter;
|
|
34
|
+
/**
|
|
35
|
+
* Inbound only (ADR 0056 §5): built-in permit expressions, the same evaluator as commands and
|
|
36
|
+
* Agent tools (sender zhin role, sender platform standing, `bot:*` Bot standing). When the
|
|
37
|
+
* message does not satisfy them the middleware is skipped for that message and the chain
|
|
38
|
+
* continues; no reply is sent. The Bot's own standing is read from the cache only (no platform
|
|
39
|
+
* lookup per message): unknown means `bot:*` is unmet.
|
|
40
|
+
*/
|
|
41
|
+
readonly permit?: readonly string[];
|
|
34
42
|
}
|
|
35
43
|
export type MiddlewareDefinition<TInput = unknown, TConfig = unknown, TAdapter extends string | undefined = string | undefined> = MiddlewareDefinitionBase<TAdapter> & ({
|
|
36
44
|
readonly target: 'inbound';
|
package/lib/definition.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertPermitSyntax } from '@zhin.js/permission';
|
|
1
2
|
const middlewareBrand = 'zhin.middleware/1';
|
|
2
3
|
export function defineMiddleware(definition) {
|
|
3
4
|
if (typeof definition.handle !== 'function') {
|
|
@@ -19,7 +20,16 @@ export function defineMiddleware(definition) {
|
|
|
19
20
|
const order = definition.order ?? 0;
|
|
20
21
|
if (!Number.isSafeInteger(order))
|
|
21
22
|
throw new TypeError('Middleware order must be a safe integer');
|
|
22
|
-
|
|
23
|
+
const permit = definition.permit;
|
|
24
|
+
if (permit !== undefined) {
|
|
25
|
+
if (!Array.isArray(permit) || !permit.every(item => typeof item === 'string'))
|
|
26
|
+
throw new TypeError('Middleware permit must be a string array');
|
|
27
|
+
if (target !== 'inbound' && permit.length)
|
|
28
|
+
throw new TypeError('Middleware permit applies to inbound middleware only');
|
|
29
|
+
assertPermitSyntax(permit, 'Middleware');
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({ ...definition, ...(permit !== undefined ? { permit: Object.freeze([...permit]) } : {}),
|
|
32
|
+
$feature: middlewareBrand, phase, target, order });
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
35
|
* Fix the plugin config type first, then define the middleware.
|
|
@@ -25,6 +25,8 @@ export interface MiddlewareDescriptor {
|
|
|
25
25
|
readonly order: number;
|
|
26
26
|
readonly adapter?: string;
|
|
27
27
|
readonly incremental?: true;
|
|
28
|
+
/** Inbound permit (ADR 0056 §5); unmet = skipped for that message. */
|
|
29
|
+
readonly permit?: readonly string[];
|
|
28
30
|
}
|
|
29
31
|
/** @internal Runtime middleware executor. */
|
|
30
32
|
export declare class MiddlewareIndex {
|
package/lib/middleware-index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCapabilityContext, bindOperationInput, operationClientAdapter, readOperationClient, readOperationSession, } from '@zhin.js/feature-kit';
|
|
2
2
|
import { continuationBrand } from './continuation.js';
|
|
3
|
+
import { permissionHostToken, toPermissionSubject } from '@zhin.js/permission';
|
|
3
4
|
/** @internal Runtime middleware executor. */
|
|
4
5
|
export class MiddlewareIndex {
|
|
5
6
|
snapshot;
|
|
@@ -17,6 +18,7 @@ export class MiddlewareIndex {
|
|
|
17
18
|
order: slot.definition.order,
|
|
18
19
|
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
19
20
|
...(slot.definition.target === 'outbound' && slot.definition.incremental === true ? { incremental: true } : {}),
|
|
21
|
+
...(slot.definition.target === 'inbound' && slot.definition.permit?.length ? { permit: slot.definition.permit } : {}),
|
|
20
22
|
slot,
|
|
21
23
|
})).sort((left, right) => compareMiddleware(left, right, topology)));
|
|
22
24
|
}
|
|
@@ -93,6 +95,8 @@ export class MiddlewareIndex {
|
|
|
93
95
|
const definition = record.slot.definition;
|
|
94
96
|
if (definition.target !== 'inbound')
|
|
95
97
|
throw new Error('Invalid inbound Middleware');
|
|
98
|
+
if (record.permit && !(await permitAllows(record.permit, input, operationSnapshot)))
|
|
99
|
+
return dispatch(index + 1);
|
|
96
100
|
const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
|
|
97
101
|
const context = {
|
|
98
102
|
...createCapabilityContext({ snapshot: operationSnapshot, ownerId: record.owner, admission: undefined, signal: readOperationSession(authorInput, record.adapter)?.signal }), input: authorInput,
|
|
@@ -160,6 +164,28 @@ export class MiddlewareIndex {
|
|
|
160
164
|
return dispatch(0);
|
|
161
165
|
}
|
|
162
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* ADR 0056 §5: the shared evaluator; the Bot's own standing comes from the cache only, so a gate
|
|
169
|
+
* never adds a platform request to a message. No Permission host = unmet (fail closed).
|
|
170
|
+
*/
|
|
171
|
+
async function permitAllows(permits, input, snapshot) {
|
|
172
|
+
let host;
|
|
173
|
+
try {
|
|
174
|
+
const value = snapshot.resources.get(snapshot.root)?.get(permissionHostToken.id);
|
|
175
|
+
host = value && typeof value.checkAll === 'function' ? value : undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
host = undefined;
|
|
179
|
+
}
|
|
180
|
+
if (!host)
|
|
181
|
+
return false;
|
|
182
|
+
try {
|
|
183
|
+
return await host.checkAll(permits, toPermissionSubject(input), { source: input, cachedOnly: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
163
189
|
/** @internal Runtime projection guard. */
|
|
164
190
|
export function isMiddlewareIndex(value) {
|
|
165
191
|
return !!value && typeof value === 'object'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/middleware",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Convention-based Middleware Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"lib"
|
|
16
|
-
"src"
|
|
15
|
+
"lib"
|
|
17
16
|
],
|
|
18
17
|
"dependencies": {
|
|
19
|
-
"@zhin.js/
|
|
20
|
-
"@zhin.js/
|
|
18
|
+
"@zhin.js/feature-kit": "1.1.16",
|
|
19
|
+
"@zhin.js/permission": "1.2.0",
|
|
20
|
+
"@zhin.js/plugin-runtime": "1.2.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.1.2",
|
package/src/continuation.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/** Internal marker only; each frame additionally checks its exact carrier identity. */
|
|
2
|
-
export const continuationBrand: unique symbol = Symbol('zhin.middleware-continuation');
|
|
3
|
-
|
|
4
|
-
/** Opaque result of `next()`. Return it to preserve the downstream author. */
|
|
5
|
-
export interface MiddlewareContinuation {
|
|
6
|
-
readonly [continuationBrand]: never;
|
|
7
|
-
}
|
package/src/definition.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Middleware authoring API consumed from `zhin.js/middleware`.
|
|
3
|
-
* @module zhin.js/middleware
|
|
4
|
-
*/
|
|
5
|
-
import type {
|
|
6
|
-
AdapterClient,
|
|
7
|
-
OperationSession,
|
|
8
|
-
CapabilityContext,
|
|
9
|
-
RegisteredAdapterName,
|
|
10
|
-
} from '@zhin.js/feature-kit';
|
|
11
|
-
import type { MiddlewareContinuation } from './continuation.js';
|
|
12
|
-
export type { MiddlewareContinuation } from './continuation.js';
|
|
13
|
-
|
|
14
|
-
const middlewareBrand = 'zhin.middleware/1' as const;
|
|
15
|
-
|
|
16
|
-
export type MiddlewarePhase = 'before-dispatch' | 'after-dispatch';
|
|
17
|
-
export type MiddlewareTarget = 'inbound' | 'outbound';
|
|
18
|
-
export type MiddlewareNext = () => Promise<MiddlewareContinuation>;
|
|
19
|
-
export type OutboundMiddlewareNext = () => Promise<void>;
|
|
20
|
-
|
|
21
|
-
export interface InboundDispatchSummary {
|
|
22
|
-
readonly matched: boolean;
|
|
23
|
-
readonly path: 'button' | 'command' | 'handler' | 'none';
|
|
24
|
-
readonly handlerId?: string;
|
|
25
|
-
readonly replied: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface MiddlewareContext<
|
|
29
|
-
TInput = unknown,
|
|
30
|
-
TConfig = unknown,
|
|
31
|
-
TAdapter extends string | undefined = undefined,
|
|
32
|
-
>
|
|
33
|
-
extends CapabilityContext<TConfig> {
|
|
34
|
-
readonly input: TInput;
|
|
35
|
-
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
36
|
-
readonly $client: AdapterClient<TAdapter>;
|
|
37
|
-
readonly session: OperationSession<TAdapter> | undefined;
|
|
38
|
-
readonly dispatch?: InboundDispatchSummary;
|
|
39
|
-
onAfterDispatch?(listener: (summary: InboundDispatchSummary) => void | Promise<void>): void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Shared metadata for the public inbound and outbound definition contracts. */
|
|
43
|
-
export interface MiddlewareDefinitionBase<
|
|
44
|
-
TAdapter extends string | undefined = string | undefined,
|
|
45
|
-
> {
|
|
46
|
-
/** @internal Runtime feature brand. */
|
|
47
|
-
readonly $feature: typeof middlewareBrand;
|
|
48
|
-
readonly phase: MiddlewarePhase;
|
|
49
|
-
readonly order: number;
|
|
50
|
-
readonly adapter?: TAdapter;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type MiddlewareDefinition<
|
|
54
|
-
TInput = unknown,
|
|
55
|
-
TConfig = unknown,
|
|
56
|
-
TAdapter extends string | undefined = string | undefined,
|
|
57
|
-
> = MiddlewareDefinitionBase<TAdapter> & (
|
|
58
|
-
| { readonly target: 'inbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
|
|
59
|
-
| { readonly target: 'outbound';
|
|
60
|
-
/** Opt in to cumulative prefixes. Previously published text, format and addressing must stay stable. */
|
|
61
|
-
readonly incremental?: true;
|
|
62
|
-
handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void> }
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
declare module '@zhin.js/plugin-runtime' {
|
|
66
|
-
interface PluginSetupContext<TConfig = unknown> {
|
|
67
|
-
addMiddleware<TInput = unknown>(
|
|
68
|
-
localName: string,
|
|
69
|
-
definition: MiddlewareDefinition<TInput, TConfig, string | undefined>,
|
|
70
|
-
): void;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Define ordered inbound or outbound middleware.
|
|
76
|
-
* Call `next()` to continue the chain; omit it to stop dispatch deliberately.
|
|
77
|
-
*
|
|
78
|
-
* @public
|
|
79
|
-
*/
|
|
80
|
-
type AuthoringForAdapter<TInput, TConfig, TAdapter extends string | undefined> =
|
|
81
|
-
Omit<MiddlewareDefinitionBase<TAdapter>, '$feature' | 'phase' | 'order'> & {
|
|
82
|
-
readonly phase?: MiddlewarePhase;
|
|
83
|
-
readonly stage?: MiddlewarePhase;
|
|
84
|
-
readonly order?: number;
|
|
85
|
-
} & (
|
|
86
|
-
| { readonly target?: 'inbound'; handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
|
|
87
|
-
| {
|
|
88
|
-
readonly target: 'outbound';
|
|
89
|
-
/** Opt in to cumulative prefixes. Previously published text, format and addressing must stay stable;
|
|
90
|
-
* do not repeat approvals, external sends or other side effects. */
|
|
91
|
-
readonly incremental?: true;
|
|
92
|
-
handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
/** @internal Shape shared with the typed IM facade; not a separate authoring API. */
|
|
97
|
-
export type MiddlewareAuthoringDefinition<TInput, TConfig> =
|
|
98
|
-
| AuthoringForAdapter<TInput, TConfig, undefined>
|
|
99
|
-
| {
|
|
100
|
-
[TAdapter in RegisteredAdapterName]: AuthoringForAdapter<TInput, TConfig, TAdapter> & {
|
|
101
|
-
readonly adapter: TAdapter;
|
|
102
|
-
}
|
|
103
|
-
}[RegisteredAdapterName];
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Returned by `defineMiddleware.withConfig<Config>()`: fixes the plugin config type while
|
|
107
|
-
* keeping inbound/outbound input inference intact.
|
|
108
|
-
*
|
|
109
|
-
* @public
|
|
110
|
-
*/
|
|
111
|
-
export interface MiddlewareDefiner<TConfig> {
|
|
112
|
-
<TInput = unknown>(
|
|
113
|
-
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target: 'outbound' }>,
|
|
114
|
-
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'outbound' }>>;
|
|
115
|
-
<TInput = unknown>(
|
|
116
|
-
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target?: 'inbound' }>,
|
|
117
|
-
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'inbound' }>>;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
121
|
-
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target: 'outbound' }>,
|
|
122
|
-
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'outbound' }>>;
|
|
123
|
-
export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
124
|
-
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target?: 'inbound' }>,
|
|
125
|
-
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'inbound' }>>;
|
|
126
|
-
export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
127
|
-
definition: MiddlewareAuthoringDefinition<TInput, TConfig>,
|
|
128
|
-
): Readonly<MiddlewareDefinition<TInput, TConfig, string | undefined>> {
|
|
129
|
-
if (typeof definition.handle !== 'function') {
|
|
130
|
-
throw new TypeError('Middleware handle must be a function');
|
|
131
|
-
}
|
|
132
|
-
const adapter = (definition as { readonly adapter?: unknown }).adapter;
|
|
133
|
-
if (adapter !== undefined
|
|
134
|
-
&& (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
135
|
-
throw new TypeError('Middleware adapter must be a non-empty string');
|
|
136
|
-
}
|
|
137
|
-
const phase = definition.phase ?? (definition as { readonly stage?: MiddlewarePhase }).stage ?? 'before-dispatch';
|
|
138
|
-
if (phase !== 'before-dispatch' && phase !== 'after-dispatch') {
|
|
139
|
-
throw new TypeError(`Invalid Middleware phase: ${String(phase)}`);
|
|
140
|
-
}
|
|
141
|
-
const target = definition.target ?? 'inbound';
|
|
142
|
-
if (target !== 'inbound' && target !== 'outbound') {
|
|
143
|
-
throw new TypeError(`Invalid Middleware target: ${String(target)}`);
|
|
144
|
-
}
|
|
145
|
-
const order = definition.order ?? 0;
|
|
146
|
-
if (!Number.isSafeInteger(order)) throw new TypeError('Middleware order must be a safe integer');
|
|
147
|
-
return Object.freeze({ ...definition, $feature: middlewareBrand, phase, target, order }) as Readonly<
|
|
148
|
-
MiddlewareDefinition<TInput, TConfig, string | undefined>
|
|
149
|
-
>;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Fix the plugin config type first, then define the middleware.
|
|
154
|
-
*/
|
|
155
|
-
defineMiddleware.withConfig = function withConfig<TConfig = unknown>(): MiddlewareDefiner<TConfig> {
|
|
156
|
-
return ((definition: MiddlewareAuthoringDefinition<unknown, TConfig>) =>
|
|
157
|
-
defineMiddleware(definition as never)) as MiddlewareDefiner<TConfig>;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/** @internal Runtime validation for convention-discovered modules. */
|
|
161
|
-
export function parseMiddlewareDefinition(value: unknown): MiddlewareDefinition {
|
|
162
|
-
if (!value || typeof value !== 'object') throw invalidMiddleware();
|
|
163
|
-
const definition = value as Partial<MiddlewareDefinition>;
|
|
164
|
-
if (
|
|
165
|
-
definition.$feature !== middlewareBrand
|
|
166
|
-
|| typeof definition.handle !== 'function'
|
|
167
|
-
|| (definition.phase !== 'before-dispatch' && definition.phase !== 'after-dispatch')
|
|
168
|
-
|| (definition.target !== 'inbound' && definition.target !== 'outbound')
|
|
169
|
-
|| !Number.isSafeInteger(definition.order)
|
|
170
|
-
|| !validAdapterName((definition as { readonly adapter?: unknown }).adapter)
|
|
171
|
-
) throw invalidMiddleware();
|
|
172
|
-
return definition as MiddlewareDefinition;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function invalidMiddleware(): TypeError {
|
|
176
|
-
return new TypeError('Middleware module must default-export defineMiddleware(...)');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function validAdapterName(adapter: unknown): boolean {
|
|
180
|
-
return adapter === undefined || (typeof adapter === 'string' && adapter.trim() !== '');
|
|
181
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ordered inbound and outbound middleware authoring contracts.
|
|
3
|
-
* @module @zhin.js/middleware
|
|
4
|
-
*/
|
|
5
|
-
/** @public Middleware authoring contract and `defineMiddleware`. */
|
|
6
|
-
export * from './definition.js';
|
|
7
|
-
/** @internal Runtime middleware projection. */
|
|
8
|
-
export * from './middleware-index.js';
|
|
9
|
-
/** @internal Feature provider installed by the Runtime composition root. */
|
|
10
|
-
export * from './provider.js';
|
|
11
|
-
export { default } from './provider.js';
|
package/src/middleware-index.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import {
|
|
3
|
-
createCapabilityContext,
|
|
4
|
-
bindOperationInput,
|
|
5
|
-
operationClientAdapter,
|
|
6
|
-
readOperationClient,
|
|
7
|
-
readOperationSession,
|
|
8
|
-
} from '@zhin.js/feature-kit';
|
|
9
|
-
import type {
|
|
10
|
-
InboundDispatchSummary,
|
|
11
|
-
MiddlewareContext,
|
|
12
|
-
MiddlewareDefinition,
|
|
13
|
-
MiddlewarePhase,
|
|
14
|
-
MiddlewareTarget,
|
|
15
|
-
OutboundMiddlewareNext,
|
|
16
|
-
} from './definition.js';
|
|
17
|
-
import { continuationBrand, type MiddlewareContinuation } from './continuation.js';
|
|
18
|
-
|
|
19
|
-
/** @internal Terminal control passed to inbound terminal callbacks to publish dispatch summary before after-dispatch unwinds. */
|
|
20
|
-
export interface InboundTerminalControl {
|
|
21
|
-
reportDispatch(summary: InboundDispatchSummary): Promise<void>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** @internal Transmission-neutral result policy supplied by the composition layer. */
|
|
25
|
-
export interface MiddlewareResultPolicy<TResult> {
|
|
26
|
-
stop(owner: PluginId): TResult;
|
|
27
|
-
replace(owner: PluginId, value: unknown, downstream: TResult | undefined): TResult;
|
|
28
|
-
summarize?(result: TResult): InboundDispatchSummary | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** @internal Runtime continuation result. */
|
|
32
|
-
export interface MiddlewareRunResult {
|
|
33
|
-
readonly owner?: PluginId;
|
|
34
|
-
readonly value?: unknown;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** @internal Runtime inventory projection. */
|
|
38
|
-
export interface MiddlewareDescriptor {
|
|
39
|
-
readonly owner: PluginId;
|
|
40
|
-
readonly name: string;
|
|
41
|
-
readonly source: string;
|
|
42
|
-
readonly phase: MiddlewarePhase;
|
|
43
|
-
readonly target: MiddlewareTarget;
|
|
44
|
-
readonly order: number;
|
|
45
|
-
readonly adapter?: string;
|
|
46
|
-
readonly incremental?: true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface MiddlewareRecord extends MiddlewareDescriptor {
|
|
50
|
-
readonly slot: Readonly<CapabilitySlot<MiddlewareDefinition>>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** @internal Runtime middleware executor. */
|
|
54
|
-
export class MiddlewareIndex {
|
|
55
|
-
readonly $projection = 'zhin.middleware-index/1' as const;
|
|
56
|
-
readonly #records: readonly MiddlewareRecord[];
|
|
57
|
-
|
|
58
|
-
constructor(
|
|
59
|
-
slots: readonly Readonly<CapabilitySlot<MiddlewareDefinition>>[],
|
|
60
|
-
private readonly snapshot: RuntimeSnapshot,
|
|
61
|
-
) {
|
|
62
|
-
const topology = topologyOrder(snapshot);
|
|
63
|
-
this.#records = Object.freeze(slots.map((slot) => Object.freeze({
|
|
64
|
-
owner: slot.owner,
|
|
65
|
-
name: slot.localName,
|
|
66
|
-
source: slot.source,
|
|
67
|
-
phase: slot.definition.phase,
|
|
68
|
-
target: slot.definition.target,
|
|
69
|
-
order: slot.definition.order,
|
|
70
|
-
...(slot.definition.adapter ? { adapter: slot.definition.adapter } : {}),
|
|
71
|
-
...(slot.definition.target === 'outbound' && slot.definition.incremental === true ? { incremental: true as const } : {}),
|
|
72
|
-
slot,
|
|
73
|
-
})).sort((left, right) => compareMiddleware(left, right, topology)));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
list(): readonly MiddlewareDescriptor[] {
|
|
77
|
-
return this.#records.map(({ slot: _slot, ...descriptor }) => descriptor);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
run<TInput>(input: TInput, terminal?: OutboundMiddlewareNext, target?: 'inbound', operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult>;
|
|
81
|
-
run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: 'outbound', operationSnapshot?: RuntimeSnapshot): Promise<void>;
|
|
82
|
-
run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: MiddlewareTarget, operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult | void>;
|
|
83
|
-
async run<TInput>(
|
|
84
|
-
input: TInput,
|
|
85
|
-
terminal: OutboundMiddlewareNext = async () => undefined,
|
|
86
|
-
target: MiddlewareTarget = 'inbound',
|
|
87
|
-
operationSnapshot: RuntimeSnapshot = this.snapshot,
|
|
88
|
-
): Promise<MiddlewareRunResult | void> {
|
|
89
|
-
if (target === 'inbound') {
|
|
90
|
-
return this.runInbound(input, async () => { await terminal(); return {}; }, {
|
|
91
|
-
stop: (owner) => ({ owner }),
|
|
92
|
-
replace: (owner, value) => ({ owner, value }),
|
|
93
|
-
}, operationSnapshot);
|
|
94
|
-
}
|
|
95
|
-
const clientAdapter = operationClientAdapter(input);
|
|
96
|
-
const records = this.#records.filter((record) =>
|
|
97
|
-
record.target === target && (!record.adapter || record.adapter === clientAdapter));
|
|
98
|
-
let cursor = -1;
|
|
99
|
-
const dispatch = async (index: number): Promise<void> => {
|
|
100
|
-
if (index <= cursor) throw new Error('Middleware next() called more than once');
|
|
101
|
-
cursor = index;
|
|
102
|
-
const record = records[index];
|
|
103
|
-
if (!record) return terminal();
|
|
104
|
-
const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
|
|
105
|
-
const context = {
|
|
106
|
-
...createCapabilityContext({ snapshot: operationSnapshot, ownerId: record.owner, admission: undefined, signal: readOperationSession(authorInput, record.adapter)?.signal }),
|
|
107
|
-
input: authorInput,
|
|
108
|
-
} as MiddlewareContext<TInput>;
|
|
109
|
-
Object.defineProperty(context, 'session', {
|
|
110
|
-
enumerable: true, get: () => readOperationSession(authorInput, record.adapter),
|
|
111
|
-
});
|
|
112
|
-
Object.defineProperty(context, '$client', {
|
|
113
|
-
enumerable: true,
|
|
114
|
-
get: () => readOperationClient(authorInput, record.adapter),
|
|
115
|
-
});
|
|
116
|
-
Object.freeze(context);
|
|
117
|
-
const definition = record.slot.definition;
|
|
118
|
-
if (definition.target !== 'outbound') throw new Error('Invalid outbound Middleware');
|
|
119
|
-
const result = await definition.handle.call(context, context, () => dispatch(index + 1));
|
|
120
|
-
if (result !== undefined) throw new TypeError('Outbound Middleware must use input.replace(), not return content');
|
|
121
|
-
};
|
|
122
|
-
await dispatch(0);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Unwinds one inbound operation before its caller performs any automatic reply. */
|
|
126
|
-
async runInbound<TInput, TResult>(
|
|
127
|
-
input: TInput,
|
|
128
|
-
terminal: (control: InboundTerminalControl) => Promise<TResult>,
|
|
129
|
-
results: MiddlewareResultPolicy<TResult>,
|
|
130
|
-
operationSnapshot: RuntimeSnapshot = this.snapshot,
|
|
131
|
-
): Promise<TResult> {
|
|
132
|
-
const clientAdapter = operationClientAdapter(input);
|
|
133
|
-
const records = this.#records.filter((record) => record.target === 'inbound'
|
|
134
|
-
&& (!record.adapter || record.adapter === clientAdapter));
|
|
135
|
-
let dispatchSummary: InboundDispatchSummary | undefined;
|
|
136
|
-
let reported = false;
|
|
137
|
-
const afterDispatchListeners: Array<(summary: InboundDispatchSummary) => void | Promise<void>> = [];
|
|
138
|
-
const reportDispatch = async (summary: InboundDispatchSummary): Promise<void> => {
|
|
139
|
-
dispatchSummary = Object.freeze({ ...summary });
|
|
140
|
-
if (reported) return;
|
|
141
|
-
reported = true;
|
|
142
|
-
for (const listener of afterDispatchListeners.splice(0)) {
|
|
143
|
-
await listener(dispatchSummary);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
const dispatch = async (index: number): Promise<TResult> => {
|
|
147
|
-
const record = records[index];
|
|
148
|
-
if (!record) {
|
|
149
|
-
const terminalResult = await terminal({ reportDispatch });
|
|
150
|
-
if (!reported) {
|
|
151
|
-
const inferred = results.summarize?.(terminalResult)
|
|
152
|
-
?? { matched: false, path: 'none' as const, replied: false };
|
|
153
|
-
await reportDispatch(inferred);
|
|
154
|
-
}
|
|
155
|
-
return terminalResult;
|
|
156
|
-
}
|
|
157
|
-
const definition = record.slot.definition;
|
|
158
|
-
if (definition.target !== 'inbound') throw new Error('Invalid inbound Middleware');
|
|
159
|
-
const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
|
|
160
|
-
const context = {
|
|
161
|
-
...createCapabilityContext({ snapshot: operationSnapshot, ownerId: record.owner, admission: undefined, signal: readOperationSession(authorInput, record.adapter)?.signal }), input: authorInput,
|
|
162
|
-
onAfterDispatch(listener: (summary: InboundDispatchSummary) => void | Promise<void>) {
|
|
163
|
-
if (typeof listener !== 'function') {
|
|
164
|
-
throw new TypeError('Middleware onAfterDispatch listener must be a function');
|
|
165
|
-
}
|
|
166
|
-
afterDispatchListeners.push(listener);
|
|
167
|
-
},
|
|
168
|
-
} as MiddlewareContext<TInput>;
|
|
169
|
-
Object.defineProperty(context, 'session', {
|
|
170
|
-
enumerable: true, get: () => readOperationSession(authorInput, record.adapter),
|
|
171
|
-
});
|
|
172
|
-
Object.defineProperty(context, '$client', {
|
|
173
|
-
enumerable: true, get: () => readOperationClient(authorInput, record.adapter),
|
|
174
|
-
});
|
|
175
|
-
Object.defineProperty(context, 'dispatch', {
|
|
176
|
-
enumerable: true, get: () => dispatchSummary,
|
|
177
|
-
});
|
|
178
|
-
Object.freeze(context);
|
|
179
|
-
let active = true;
|
|
180
|
-
let nextCalled = false;
|
|
181
|
-
let nextTask: Promise<TResult> | undefined;
|
|
182
|
-
let carrier: MiddlewareContinuation | undefined;
|
|
183
|
-
const next = (): Promise<MiddlewareContinuation> => {
|
|
184
|
-
if (!active) throw new Error('Middleware next() scope has ended');
|
|
185
|
-
if (nextCalled) throw new Error('Middleware next() called more than once');
|
|
186
|
-
nextCalled = true;
|
|
187
|
-
nextTask = dispatch(index + 1);
|
|
188
|
-
// Attach a rejection observer immediately even when an author forgets await.
|
|
189
|
-
void nextTask.catch(() => undefined);
|
|
190
|
-
const continuation = nextTask.then(() => {
|
|
191
|
-
carrier = Object.freeze({ [continuationBrand]: undefined as never });
|
|
192
|
-
return carrier;
|
|
193
|
-
});
|
|
194
|
-
void continuation.catch(() => undefined);
|
|
195
|
-
return continuation;
|
|
196
|
-
};
|
|
197
|
-
let returned: unknown;
|
|
198
|
-
try {
|
|
199
|
-
returned = await definition.handle.call(context, context, next);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
// Drain an admitted child before the owning generation lease can close.
|
|
202
|
-
await nextTask?.catch(() => undefined);
|
|
203
|
-
throw error;
|
|
204
|
-
} finally {
|
|
205
|
-
active = false;
|
|
206
|
-
}
|
|
207
|
-
const downstream = nextTask ? await nextTask : undefined;
|
|
208
|
-
if (returned === undefined) return nextTask ?? results.stop(record.owner);
|
|
209
|
-
if (returned && typeof returned === 'object' && continuationBrand in returned) {
|
|
210
|
-
if (returned !== carrier) throw new Error('Middleware continuation belongs to another frame or operation');
|
|
211
|
-
if (!nextTask) throw new Error('Middleware continuation has no downstream operation');
|
|
212
|
-
return nextTask;
|
|
213
|
-
}
|
|
214
|
-
return results.replace(record.owner, returned, downstream);
|
|
215
|
-
};
|
|
216
|
-
return dispatch(0);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** @internal Runtime projection guard. */
|
|
221
|
-
export function isMiddlewareIndex(value: unknown): value is MiddlewareIndex {
|
|
222
|
-
return !!value && typeof value === 'object'
|
|
223
|
-
&& (value as { readonly $projection?: unknown }).$projection === 'zhin.middleware-index/1';
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function compareMiddleware(
|
|
227
|
-
left: MiddlewareRecord,
|
|
228
|
-
right: MiddlewareRecord,
|
|
229
|
-
topology: ReadonlyMap<PluginId, number>,
|
|
230
|
-
): number {
|
|
231
|
-
return phaseOrder(left.phase) - phaseOrder(right.phase)
|
|
232
|
-
|| left.order - right.order
|
|
233
|
-
|| (topology.get(left.owner) ?? Number.MAX_SAFE_INTEGER)
|
|
234
|
-
- (topology.get(right.owner) ?? Number.MAX_SAFE_INTEGER)
|
|
235
|
-
|| left.slot.id.localeCompare(right.slot.id);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function phaseOrder(phase: MiddlewarePhase): number {
|
|
239
|
-
return phase === 'before-dispatch' ? 0 : 1;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function topologyOrder(snapshot: RuntimeSnapshot): ReadonlyMap<PluginId, number> {
|
|
243
|
-
const result = new Map<PluginId, number>();
|
|
244
|
-
const visit = (owner: PluginId): void => {
|
|
245
|
-
if (result.has(owner)) return;
|
|
246
|
-
result.set(owner, result.size);
|
|
247
|
-
for (const child of snapshot.tree.get(owner)?.children ?? []) visit(child);
|
|
248
|
-
};
|
|
249
|
-
visit(snapshot.root);
|
|
250
|
-
return result;
|
|
251
|
-
}
|
package/src/provider.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { featureId } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { capture, captured, defineFeatureProvider, directoryModules } from '@zhin.js/feature-kit';
|
|
3
|
-
import { parseMiddlewareDefinition } from './definition.js';
|
|
4
|
-
import { MiddlewareIndex } from './middleware-index.js';
|
|
5
|
-
|
|
6
|
-
/** @internal Runtime Feature identity. */
|
|
7
|
-
export const middlewareFeatureId = featureId('zhin.middleware');
|
|
8
|
-
|
|
9
|
-
/** @internal Runtime Feature provider. */
|
|
10
|
-
const middlewareFeature = defineFeatureProvider({
|
|
11
|
-
protocol: 1,
|
|
12
|
-
id: middlewareFeatureId,
|
|
13
|
-
authoring: {
|
|
14
|
-
setupMethod: 'addMiddleware',
|
|
15
|
-
conventions: [directoryModules({
|
|
16
|
-
id: 'middlewares-index',
|
|
17
|
-
extensions: ['ts', 'tsx', 'js', 'mjs', 'cjs'],
|
|
18
|
-
layouts: [{ segments: ['middlewares', capture('name')], localName: (values) => captured(values, 'name') }],
|
|
19
|
-
})],
|
|
20
|
-
validate: parseMiddlewareDefinition,
|
|
21
|
-
},
|
|
22
|
-
runtime: {
|
|
23
|
-
project(slots, context) {
|
|
24
|
-
return { value: new MiddlewareIndex(slots, context.snapshot) };
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
export { middlewareFeature };
|
|
30
|
-
export default middlewareFeature;
|