@zhin.js/plugin-runtime 1.1.16 → 1.1.18
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/lib/admission.d.ts +15 -3
- package/lib/admission.js +10 -5
- package/lib/data-migration.d.ts +105 -0
- package/lib/data-migration.js +16 -0
- package/lib/database-host.d.ts +7 -2
- package/lib/database-host.js +4 -2
- package/lib/i18n.d.ts +24 -0
- package/lib/i18n.js +38 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +8 -0
- package/lib/outbound-host.d.ts +41 -11
- package/lib/outbound-host.js +6 -1
- package/lib/plugin-console-rpc.d.ts +96 -0
- package/lib/plugin-console-rpc.js +69 -0
- package/lib/plugin-events.d.ts +47 -0
- package/lib/plugin-events.js +30 -0
- package/lib/plugin-tasks.d.ts +11 -1
- package/lib/plugin-verification.d.ts +5 -0
- package/lib/plugin.d.ts +6 -1
- package/lib/plugin.js +2 -0
- package/lib/relational-state.d.ts +2 -0
- package/lib/relational-state.js +8 -2
- package/lib/relational-target-migration.d.ts +68 -0
- package/lib/relational-target-migration.js +571 -0
- package/lib/root-config-document.js +19 -3
- package/lib/root-controller.d.ts +2 -2
- package/lib/root-controller.js +2 -0
- package/lib/snapshot.d.ts +22 -5
- package/lib/snapshot.js +60 -14
- package/lib/token.d.ts +5 -1
- package/lib/token.js +13 -0
- package/package.json +1 -1
package/lib/admission.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export declare class GenerationAdmissionGate {
|
|
2
2
|
#private;
|
|
3
3
|
get active(): boolean;
|
|
4
|
+
/** Root closure fact, bound at first publication and retained after generation retirement. */
|
|
5
|
+
get stopSignal(): AbortSignal | undefined;
|
|
4
6
|
acquire(): (() => void) | undefined;
|
|
5
7
|
/** Observe the first commit which publishes this generation. */
|
|
6
8
|
onActivate(listener: () => void): () => void;
|
|
7
9
|
/**
|
|
8
10
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
9
|
-
* synchronous and must not fail; it
|
|
10
|
-
*
|
|
11
|
+
* synchronous and must not fail. It closes new admission; it does not decide
|
|
12
|
+
* whether already-owned transports or work should end. Use stopSignal for Root shutdown.
|
|
11
13
|
*/
|
|
12
14
|
onDeactivate(listener: () => void): () => void;
|
|
13
15
|
enter<T>(operation: () => T): T | undefined;
|
|
@@ -26,4 +28,14 @@ export interface GenerationAdmissionSource {
|
|
|
26
28
|
}
|
|
27
29
|
export declare function bindGenerationAdmission<T>(value: T, gate: GenerationAdmissionGate): T;
|
|
28
30
|
export declare function collectGenerationAdmissions(values: Iterable<unknown>): ReadonlySet<GenerationAdmissionGate>;
|
|
29
|
-
|
|
31
|
+
/** One SnapshotStore-owned publication; all gates share that Root's explicit close fact. */
|
|
32
|
+
interface GenerationAdmissionTransition {
|
|
33
|
+
readonly previous: ReadonlySet<GenerationAdmissionGate>;
|
|
34
|
+
readonly next: ReadonlySet<GenerationAdmissionGate>;
|
|
35
|
+
readonly owner: object;
|
|
36
|
+
readonly publishNext: () => void;
|
|
37
|
+
readonly acquireNext?: () => () => void;
|
|
38
|
+
readonly stopSignal: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
export declare function replaceGenerationAdmissions({ previous, next, owner, publishNext, acquireNext, stopSignal }: GenerationAdmissionTransition): void;
|
|
41
|
+
export {};
|
package/lib/admission.js
CHANGED
|
@@ -5,13 +5,14 @@ let admissionLifecycle;
|
|
|
5
5
|
export class GenerationAdmissionGate {
|
|
6
6
|
#state = { active: false };
|
|
7
7
|
#owner;
|
|
8
|
+
#stopSignal;
|
|
8
9
|
#activationListeners = new Set();
|
|
9
10
|
#deactivationListeners = new Set();
|
|
10
11
|
static {
|
|
11
12
|
admissionLifecycle = {
|
|
12
13
|
assertClaimable: (gate, owner) => gate.#assertClaimable(owner),
|
|
13
14
|
assertOwner: (gate, owner) => gate.#assertOwner(owner),
|
|
14
|
-
claim: (gate, owner) => { gate.#owner = owner; },
|
|
15
|
+
claim: (gate, owner, stopSignal) => { gate.#owner = owner; gate.#stopSignal = stopSignal; },
|
|
15
16
|
activate: (gate, acquire) => gate.#activate(acquire),
|
|
16
17
|
deactivate: (gate) => gate.#deactivate(),
|
|
17
18
|
notifyActivation: (gate) => gate.#notifyActivation(),
|
|
@@ -21,6 +22,10 @@ export class GenerationAdmissionGate {
|
|
|
21
22
|
get active() {
|
|
22
23
|
return this.#state.active;
|
|
23
24
|
}
|
|
25
|
+
/** Root closure fact, bound at first publication and retained after generation retirement. */
|
|
26
|
+
get stopSignal() {
|
|
27
|
+
return this.#stopSignal;
|
|
28
|
+
}
|
|
24
29
|
acquire() {
|
|
25
30
|
return this.#state.active ? this.#state.acquire?.() : undefined;
|
|
26
31
|
}
|
|
@@ -35,8 +40,8 @@ export class GenerationAdmissionGate {
|
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
42
|
* Observe retirement of the generation that owns this gate. The callback is
|
|
38
|
-
* synchronous and must not fail; it
|
|
39
|
-
*
|
|
43
|
+
* synchronous and must not fail. It closes new admission; it does not decide
|
|
44
|
+
* whether already-owned transports or work should end. Use stopSignal for Root shutdown.
|
|
40
45
|
*/
|
|
41
46
|
onDeactivate(listener) {
|
|
42
47
|
if (!this.active) {
|
|
@@ -136,7 +141,7 @@ export function collectGenerationAdmissions(values) {
|
|
|
136
141
|
}
|
|
137
142
|
return result;
|
|
138
143
|
}
|
|
139
|
-
export function replaceGenerationAdmissions(previous, next, owner, publishNext, acquireNext) {
|
|
144
|
+
export function replaceGenerationAdmissions({ previous, next, owner, publishNext, acquireNext, stopSignal }) {
|
|
140
145
|
// Validate the complete switch before changing ownership or visibility. A
|
|
141
146
|
// rejected cross-Root gate must leave both generations exactly untouched.
|
|
142
147
|
for (const gate of next)
|
|
@@ -148,7 +153,7 @@ export function replaceGenerationAdmissions(previous, next, owner, publishNext,
|
|
|
148
153
|
// potentially rejecting ownership check has completed before publication.
|
|
149
154
|
publishNext();
|
|
150
155
|
for (const gate of next)
|
|
151
|
-
admissionLifecycle.claim(gate, owner);
|
|
156
|
+
admissionLifecycle.claim(gate, owner, stopSignal);
|
|
152
157
|
for (const gate of previous) {
|
|
153
158
|
if (next.has(gate))
|
|
154
159
|
continue;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { DeliveryTarget } from './delivery-target.js';
|
|
2
|
+
import type { GenerationAdmissionSource } from './admission.js';
|
|
3
|
+
import type { JsonValue } from './persistent-state.js';
|
|
4
|
+
/** Read-only, owner-namespaced historical data; candidate IDs are opaque to the Host. */
|
|
5
|
+
export interface DataMigrationCandidate {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly domain: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly sourceVersion: string;
|
|
10
|
+
readonly recordCount: number;
|
|
11
|
+
readonly beforeDigest: string;
|
|
12
|
+
readonly status: 'unresolved' | 'canonical' | 'corrupt';
|
|
13
|
+
/** Display facts only; never a preselected account or recovered authorization. */
|
|
14
|
+
readonly summary: Readonly<Record<string, JsonValue>>;
|
|
15
|
+
/** False when the configured persistence cannot supply the required atomic unit. */
|
|
16
|
+
readonly executable: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface DataMigrationInventory {
|
|
19
|
+
readonly revision: string;
|
|
20
|
+
readonly candidates: readonly DataMigrationCandidate[];
|
|
21
|
+
}
|
|
22
|
+
export interface DataMigrationRequest {
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export interface PrepareDataMigrationRequest extends DataMigrationRequest {
|
|
26
|
+
/** Host-generated operation UUID; reused to reconcile a lost response. */
|
|
27
|
+
readonly planId: string;
|
|
28
|
+
readonly candidateId: string;
|
|
29
|
+
readonly expectedDigest: string;
|
|
30
|
+
/** Explicit administrator selection, validated by the Host against the account directory. */
|
|
31
|
+
readonly target: DeliveryTarget;
|
|
32
|
+
}
|
|
33
|
+
/** Domain-prepared exact unit; the Host persists the complete plan before admitting any mutation. */
|
|
34
|
+
export interface DataMigrationPlan {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly candidateId: string;
|
|
37
|
+
readonly domain: string;
|
|
38
|
+
readonly sourceVersion: string;
|
|
39
|
+
readonly beforeDigest: string;
|
|
40
|
+
readonly target: DeliveryTarget;
|
|
41
|
+
readonly recordCount: number;
|
|
42
|
+
/** Full lossless before-images and exact domain writes, not a 64 KiB state-store value. */
|
|
43
|
+
readonly data: JsonValue;
|
|
44
|
+
readonly description: readonly string[];
|
|
45
|
+
/** These facts require fresh authorization and must never be activated by a data migration. */
|
|
46
|
+
readonly authorizationCandidates: readonly JsonValue[];
|
|
47
|
+
}
|
|
48
|
+
export interface ApplyDataMigrationRequest extends DataMigrationRequest {
|
|
49
|
+
readonly plan: DataMigrationPlan;
|
|
50
|
+
}
|
|
51
|
+
/** Confirmed by the same atomic domain unit as the writes; unknown commits are never blindly retried. */
|
|
52
|
+
export interface DataMigrationReceipt {
|
|
53
|
+
/** Distinguishes an original commit from its explicit reversal during reconciliation. */
|
|
54
|
+
readonly operation: 'apply' | 'restore';
|
|
55
|
+
readonly planId: string;
|
|
56
|
+
readonly candidateId: string;
|
|
57
|
+
readonly beforeDigest: string;
|
|
58
|
+
readonly afterDigest: string;
|
|
59
|
+
readonly recordCount: number;
|
|
60
|
+
readonly status: 'applied' | 'already_applied';
|
|
61
|
+
readonly maintenance: 'paused';
|
|
62
|
+
}
|
|
63
|
+
export interface ResumeDataMigrationRequest extends DataMigrationRequest {
|
|
64
|
+
readonly plan: DataMigrationPlan;
|
|
65
|
+
}
|
|
66
|
+
export interface RestoreDataMigrationRequest extends ResumeDataMigrationRequest {
|
|
67
|
+
readonly receipt: DataMigrationReceipt;
|
|
68
|
+
}
|
|
69
|
+
export interface InspectDataMigrationRequest extends DataMigrationRequest {
|
|
70
|
+
readonly plan: DataMigrationPlan;
|
|
71
|
+
}
|
|
72
|
+
/** Verified current rows plus the durable maintenance journal, never inferred from a missing receipt. */
|
|
73
|
+
export interface DataMigrationInspection {
|
|
74
|
+
readonly data: 'not_applied' | 'applied' | 'restored' | 'conflict' | 'unknown';
|
|
75
|
+
readonly maintenance: 'paused' | 'resumed' | 'unknown';
|
|
76
|
+
/** True only for paused, drained scopes with a verified current recovery baseline; never authorizes data writes. */
|
|
77
|
+
readonly resumable: boolean;
|
|
78
|
+
/** A committed write receipt is not evidence that maintenance has resumed. */
|
|
79
|
+
readonly receipt?: DataMigrationReceipt;
|
|
80
|
+
}
|
|
81
|
+
/** Plugin owns record semantics, maintenance admission/drain and the existing transactional store. */
|
|
82
|
+
export interface PluginDataMigrationProvider extends GenerationAdmissionSource {
|
|
83
|
+
readonly title: string;
|
|
84
|
+
preview(request?: DataMigrationRequest): Promise<DataMigrationInventory>;
|
|
85
|
+
prepare(request: PrepareDataMigrationRequest): Promise<DataMigrationPlan>;
|
|
86
|
+
/** Enter the selected scope's maintenance, drain work, recheck and commit; leave it paused. */
|
|
87
|
+
apply(request: ApplyDataMigrationRequest): Promise<DataMigrationReceipt>;
|
|
88
|
+
/** Pure authoritative inspection; an in-flight mutation or unprovable state remains unknown. */
|
|
89
|
+
inspect(request: InspectDataMigrationRequest): Promise<DataMigrationInspection>;
|
|
90
|
+
/** Explicit resumption never starts an account, replays delivery or extends a game deadline. */
|
|
91
|
+
resume(request: ResumeDataMigrationRequest): Promise<void>;
|
|
92
|
+
/** Restore only when the actual rows still match the confirmed after-image; remain paused. */
|
|
93
|
+
restore(request: RestoreDataMigrationRequest): Promise<DataMigrationReceipt>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* commit_unknown means the data commit result is unconfirmed. invalid_input/conflict/unsupported/
|
|
97
|
+
* not_found guarantee no data commit by this invocation, but do not imply maintenance was released.
|
|
98
|
+
* unavailable does not establish either outcome. Inspect before recovery; never blindly replay.
|
|
99
|
+
*/
|
|
100
|
+
export declare class DataMigrationError extends Error {
|
|
101
|
+
readonly code: 'invalid_input' | 'conflict' | 'unsupported' | 'unavailable' | 'not_found' | 'commit_unknown';
|
|
102
|
+
constructor(code: 'invalid_input' | 'conflict' | 'unsupported' | 'unavailable' | 'not_found' | 'commit_unknown', message: string);
|
|
103
|
+
}
|
|
104
|
+
/** Existing owner Resource discovery, not a process-global migration registry. */
|
|
105
|
+
export declare const pluginDataMigrationProviderToken: import("./token.js").Token<PluginDataMigrationProvider>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createToken } from './token.js';
|
|
2
|
+
/**
|
|
3
|
+
* commit_unknown means the data commit result is unconfirmed. invalid_input/conflict/unsupported/
|
|
4
|
+
* not_found guarantee no data commit by this invocation, but do not imply maintenance was released.
|
|
5
|
+
* unavailable does not establish either outcome. Inspect before recovery; never blindly replay.
|
|
6
|
+
*/
|
|
7
|
+
export class DataMigrationError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'DataMigrationError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Existing owner Resource discovery, not a process-global migration registry. */
|
|
16
|
+
export const pluginDataMigrationProviderToken = createToken('zhin.plugin-data-migration', 'Plugin-owned historical data migration, controlled by authenticated Host operations');
|
package/lib/database-host.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export interface DatabaseHostTable {
|
|
|
44
44
|
readonly name: string;
|
|
45
45
|
readonly columns?: Readonly<Record<string, unknown>>;
|
|
46
46
|
}
|
|
47
|
+
/** Explicit, additive upgrades applied before SQL models become available. */
|
|
48
|
+
export interface DatabaseTableOptions {
|
|
49
|
+
/** Names from this table's definition. Existing columns and data remain untouched. */
|
|
50
|
+
readonly addColumns?: readonly string[];
|
|
51
|
+
}
|
|
47
52
|
export interface DatabaseHostSelectResult {
|
|
48
53
|
readonly rows: readonly unknown[];
|
|
49
54
|
readonly total: number;
|
|
@@ -83,7 +88,7 @@ export interface DatabaseHost {
|
|
|
83
88
|
readonly dialect: string;
|
|
84
89
|
readonly started: boolean;
|
|
85
90
|
/** Register a table schema before `start` (column defs match `@zhin.js/database`). */
|
|
86
|
-
define(name: string, definition: Record<string, unknown
|
|
91
|
+
define(name: string, definition: Record<string, unknown>, options?: DatabaseTableOptions): void;
|
|
87
92
|
/** Names of tables registered via `define` (Console `db:tables`). */
|
|
88
93
|
tables(): readonly string[];
|
|
89
94
|
models: {
|
|
@@ -129,7 +134,7 @@ export declare class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
|
129
134
|
get [generationAdmissionSource](): readonly GenerationAdmissionGate[];
|
|
130
135
|
get dialect(): string;
|
|
131
136
|
get started(): boolean;
|
|
132
|
-
define(name: string, definition: Record<string, unknown
|
|
137
|
+
define(name: string, definition: Record<string, unknown>, options?: DatabaseTableOptions): void;
|
|
133
138
|
tables(): readonly string[];
|
|
134
139
|
transaction<T>(callback: (transaction: DatabaseHostTransaction) => Promise<T>): Promise<T>;
|
|
135
140
|
}
|
package/lib/database-host.js
CHANGED
|
@@ -54,8 +54,10 @@ export class PluginDatabaseHost extends OwnerScopedResourceHost {
|
|
|
54
54
|
get started() {
|
|
55
55
|
return this.#host.started;
|
|
56
56
|
}
|
|
57
|
-
define(name, definition) {
|
|
58
|
-
this.#operation.setup(() =>
|
|
57
|
+
define(name, definition, options) {
|
|
58
|
+
this.#operation.setup(() => options === undefined
|
|
59
|
+
? this.#host.define(this.qualify(name), definition)
|
|
60
|
+
: this.#host.define(this.qualify(name), definition, options));
|
|
59
61
|
}
|
|
60
62
|
tables() {
|
|
61
63
|
return this.#operation.setup(() => Object.freeze(this.#host.tables().flatMap((name) => {
|
package/lib/i18n.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeliveryTarget } from './delivery-target.js';
|
|
2
|
+
/** Supported display languages. Time zone and command identities are independent. */
|
|
3
|
+
export type Locale = 'zh-CN' | 'en';
|
|
4
|
+
export declare const DEFAULT_LOCALE: Locale;
|
|
5
|
+
export declare const SUPPORTED_LOCALES: readonly ["zh-CN", "en"];
|
|
6
|
+
export declare function isLocale(value: unknown): value is Locale;
|
|
7
|
+
/** Resolve an absent display preference; configuration writes validate with isLocale. */
|
|
8
|
+
export declare function resolveLocale(value: unknown): Locale;
|
|
9
|
+
/** Immutable project language, projected with the operation's generation. */
|
|
10
|
+
export declare const projectLocaleToken: import("./token.js").Token<Locale>;
|
|
11
|
+
/** Root-owned preferences; identity never comes from display names or profile text. */
|
|
12
|
+
export interface UserLocalePreferences {
|
|
13
|
+
get(endpoint: DeliveryTarget['endpoint'], senderId: string): Promise<Locale | undefined>;
|
|
14
|
+
set(endpoint: DeliveryTarget['endpoint'], senderId: string, locale: Locale): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare const userLocalePreferencesToken: import("./token.js").Token<UserLocalePreferences>;
|
|
17
|
+
export type MessageCatalog = Readonly<Record<string, Readonly<Record<Locale, string>>>>;
|
|
18
|
+
export type MessageValues = Readonly<Record<string, string | number>>;
|
|
19
|
+
/**
|
|
20
|
+
* A package-owned, complete bilingual catalog. No global language or registration.
|
|
21
|
+
* Placeholder substitution is one pass: user text cannot become another template.
|
|
22
|
+
* Values remain plain text; HTML/JSX escaping belongs to the existing renderer.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createMessages<const T extends MessageCatalog>(catalog: T): (locale: Locale | undefined, key: keyof T & string, values?: MessageValues) => string;
|
package/lib/i18n.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createToken } from './token.js';
|
|
2
|
+
export const DEFAULT_LOCALE = 'zh-CN';
|
|
3
|
+
export const SUPPORTED_LOCALES = Object.freeze(['zh-CN', 'en']);
|
|
4
|
+
export function isLocale(value) {
|
|
5
|
+
return value === 'zh-CN' || value === 'en';
|
|
6
|
+
}
|
|
7
|
+
/** Resolve an absent display preference; configuration writes validate with isLocale. */
|
|
8
|
+
export function resolveLocale(value) {
|
|
9
|
+
return isLocale(value) ? value : DEFAULT_LOCALE;
|
|
10
|
+
}
|
|
11
|
+
/** Immutable project language, projected with the operation's generation. */
|
|
12
|
+
export const projectLocaleToken = createToken('zhin.project-locale');
|
|
13
|
+
export const userLocalePreferencesToken = createToken('zhin.user-locale-preferences');
|
|
14
|
+
/**
|
|
15
|
+
* A package-owned, complete bilingual catalog. No global language or registration.
|
|
16
|
+
* Placeholder substitution is one pass: user text cannot become another template.
|
|
17
|
+
* Values remain plain text; HTML/JSX escaping belongs to the existing renderer.
|
|
18
|
+
*/
|
|
19
|
+
export function createMessages(catalog) {
|
|
20
|
+
const entries = Object.freeze(Object.fromEntries(Object.entries(catalog).map(([key, value]) => {
|
|
21
|
+
const keys = (text) => [...new Set([...text.matchAll(/\{([a-zA-Z][\w]*)\}/gu)].map(match => match[1]))].sort();
|
|
22
|
+
if (typeof value['zh-CN'] !== 'string' || typeof value.en !== 'string'
|
|
23
|
+
|| JSON.stringify(keys(value['zh-CN'])) !== JSON.stringify(keys(value.en))) {
|
|
24
|
+
throw new TypeError(`Invalid bilingual message catalog entry: ${key}`);
|
|
25
|
+
}
|
|
26
|
+
return [key, Object.freeze({ ...value })];
|
|
27
|
+
})));
|
|
28
|
+
return (locale, key, values = {}) => {
|
|
29
|
+
const entry = entries[key];
|
|
30
|
+
if (!Object.hasOwn(entries, key) || !entry)
|
|
31
|
+
throw new TypeError(`Unknown message catalog entry: ${key}`);
|
|
32
|
+
return entry[resolveLocale(locale)].replace(/\{([a-zA-Z][\w]*)\}/gu, (_match, name) => {
|
|
33
|
+
if (!Object.hasOwn(values, name))
|
|
34
|
+
throw new TypeError(`Missing message value: ${name}`);
|
|
35
|
+
return String(values[name]);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
/** @internal Runtime 内部:CapabilitySlot(feature 与 projection 之间的载体)。 */
|
|
6
6
|
export * from './capability.js';
|
|
7
7
|
export * from './dispose.js';
|
|
8
|
+
/** @internal Shared accepted-owner frame; no additional admission or task registry. */
|
|
9
|
+
export { OwnerOperation } from './owner-operation.js';
|
|
10
|
+
/** @internal Runtime-owned delivery authority; author contexts expose subscriptions only. */
|
|
11
|
+
export { createPluginEventChannel, type PluginEventChannel } from './plugin-events.js';
|
|
8
12
|
export * from './identity.js';
|
|
9
13
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, type GenerationAdmissionBindable, type GenerationAdmissionSource, } from './admission.js';
|
|
10
14
|
export * from './handoff.js';
|
|
@@ -12,6 +16,7 @@ export * from './handoff.js';
|
|
|
12
16
|
export * from './plugin.js';
|
|
13
17
|
/** @public Root configuration contract shared by Runtime and composition tooling. */
|
|
14
18
|
export * from './plugin-configuration.js';
|
|
19
|
+
export * from './i18n.js';
|
|
15
20
|
export * from './root-config-file.js';
|
|
16
21
|
export * from './root-config-document.js';
|
|
17
22
|
/** @internal Runtime 内部:RootRuntime / RootController。 */
|
|
@@ -39,3 +44,6 @@ export * from './system-log.js';
|
|
|
39
44
|
export * from './plugin-verification.js';
|
|
40
45
|
export * from './plugin-tasks.js';
|
|
41
46
|
export * from './task-storage.js';
|
|
47
|
+
export * from './plugin-console-rpc.js';
|
|
48
|
+
export * from './data-migration.js';
|
|
49
|
+
export * from './relational-target-migration.js';
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
/** @internal Runtime 内部:CapabilitySlot(feature 与 projection 之间的载体)。 */
|
|
6
6
|
export * from './capability.js';
|
|
7
7
|
export * from './dispose.js';
|
|
8
|
+
/** @internal Shared accepted-owner frame; no additional admission or task registry. */
|
|
9
|
+
export { OwnerOperation } from './owner-operation.js';
|
|
10
|
+
/** @internal Runtime-owned delivery authority; author contexts expose subscriptions only. */
|
|
11
|
+
export { createPluginEventChannel } from './plugin-events.js';
|
|
8
12
|
export * from './identity.js';
|
|
9
13
|
export { GenerationAdmissionGate, bindGenerationAdmission, createGenerationAdmissionGate, generationAdmissionBinder, generationAdmissionSource, } from './admission.js';
|
|
10
14
|
export * from './handoff.js';
|
|
@@ -12,6 +16,7 @@ export * from './handoff.js';
|
|
|
12
16
|
export * from './plugin.js';
|
|
13
17
|
/** @public Root configuration contract shared by Runtime and composition tooling. */
|
|
14
18
|
export * from './plugin-configuration.js';
|
|
19
|
+
export * from './i18n.js';
|
|
15
20
|
export * from './root-config-file.js';
|
|
16
21
|
export * from './root-config-document.js';
|
|
17
22
|
/** @internal Runtime 内部:RootRuntime / RootController。 */
|
|
@@ -39,3 +44,6 @@ export * from './system-log.js';
|
|
|
39
44
|
export * from './plugin-verification.js';
|
|
40
45
|
export * from './plugin-tasks.js';
|
|
41
46
|
export * from './task-storage.js';
|
|
47
|
+
export * from './plugin-console-rpc.js';
|
|
48
|
+
export * from './data-migration.js';
|
|
49
|
+
export * from './relational-target-migration.js';
|
package/lib/outbound-host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DeliveryTarget } from './delivery-target.js';
|
|
2
2
|
import type { PluginId } from './identity.js';
|
|
3
|
+
import type { Locale } from './i18n.js';
|
|
3
4
|
import { generationAdmissionBinder, generationAdmissionSource, type GenerationAdmissionGate } from './admission.js';
|
|
4
5
|
/**
|
|
5
6
|
* 未锚定 endpoint 的会话地址(结构对齐 `@zhin.js/im-contract` 的
|
|
@@ -21,28 +22,24 @@ export interface OutboundConversation {
|
|
|
21
22
|
* `sendEndpointMessage` (resolve adapter/endpoint → AdapterIndex.send).
|
|
22
23
|
*/
|
|
23
24
|
export interface OutboundSendInput {
|
|
25
|
+
/** Language captured by the originating operation or persisted task. */
|
|
26
|
+
readonly locale?: Locale;
|
|
24
27
|
readonly adapter: string;
|
|
25
28
|
readonly endpointKey: string;
|
|
26
29
|
readonly conversation: OutboundConversation;
|
|
27
30
|
readonly content: string;
|
|
28
31
|
}
|
|
29
32
|
export interface OutboundReactionInput {
|
|
30
|
-
readonly adapter: string;
|
|
31
|
-
readonly endpointKey: string;
|
|
32
33
|
readonly message: OutboundMessage;
|
|
33
34
|
readonly emoji: string;
|
|
34
35
|
readonly sceneType?: string;
|
|
35
36
|
readonly channelId?: string;
|
|
36
37
|
}
|
|
37
38
|
export interface OutboundRemoveReactionInput {
|
|
38
|
-
readonly adapter: string;
|
|
39
|
-
readonly endpointKey: string;
|
|
40
39
|
readonly message: OutboundMessage;
|
|
41
40
|
readonly reactionId: string;
|
|
42
41
|
}
|
|
43
42
|
export interface OutboundRecallInput {
|
|
44
|
-
readonly adapter: string;
|
|
45
|
-
readonly endpointKey: string;
|
|
46
43
|
readonly message: OutboundMessage;
|
|
47
44
|
}
|
|
48
45
|
export type OutboundEndpointOperation = 'recall' | 'edit' | 'reaction' | 'typing';
|
|
@@ -53,11 +50,11 @@ export interface OutboundEndpointInput {
|
|
|
53
50
|
export interface OutboundEndpointCapabilities {
|
|
54
51
|
readonly operations: readonly OutboundEndpointOperation[];
|
|
55
52
|
}
|
|
56
|
-
export interface OutboundEditInput
|
|
53
|
+
export interface OutboundEditInput {
|
|
57
54
|
readonly message: OutboundMessage;
|
|
58
55
|
readonly content: unknown;
|
|
59
56
|
}
|
|
60
|
-
export interface OutboundTypingInput
|
|
57
|
+
export interface OutboundTypingInput {
|
|
61
58
|
readonly conversation: OutboundMessage['conversation'];
|
|
62
59
|
readonly active?: boolean;
|
|
63
60
|
}
|
|
@@ -71,9 +68,38 @@ export interface OutboundMessage {
|
|
|
71
68
|
}>;
|
|
72
69
|
readonly id: string;
|
|
73
70
|
}
|
|
71
|
+
/** Retain a real source Client while its inbound operation is active. Never serialize this input. */
|
|
72
|
+
export interface OutboundCaptureInput {
|
|
73
|
+
readonly target: DeliveryTarget;
|
|
74
|
+
readonly client: unknown;
|
|
75
|
+
readonly signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
/** Messaging effects bound to one source incarnation, independent of reusable account names. */
|
|
78
|
+
export interface CapturedOutboundOperation {
|
|
79
|
+
readonly target: DeliveryTarget;
|
|
80
|
+
readonly signal: AbortSignal;
|
|
81
|
+
send(content: unknown, options?: Readonly<{
|
|
82
|
+
locale?: Locale;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}>): Promise<string | null>;
|
|
85
|
+
recall?(message: OutboundMessage): Promise<void>;
|
|
86
|
+
edit?(message: OutboundMessage, content: unknown): Promise<string | null>;
|
|
87
|
+
addReaction?(message: OutboundMessage, emoji: string, hint?: Readonly<{
|
|
88
|
+
sceneType?: string;
|
|
89
|
+
channelId?: string;
|
|
90
|
+
}>): Promise<string | null>;
|
|
91
|
+
removeReaction?(message: OutboundMessage, reactionId: string): Promise<void>;
|
|
92
|
+
typing?(conversation: OutboundMessage['conversation'], active?: boolean): Promise<void>;
|
|
93
|
+
/** Release the captured source and Root leases on every completion path. */
|
|
94
|
+
release(): void;
|
|
95
|
+
}
|
|
74
96
|
export interface OutboundHost {
|
|
97
|
+
/** Creates source-bound effects; ordinary persisted sends continue using sendTarget. */
|
|
98
|
+
captureOperation?(input: OutboundCaptureInput): CapturedOutboundOperation;
|
|
75
99
|
/** Sends to persisted exact identities, rejecting stale addresses instead of aliasing another bot. */
|
|
76
|
-
sendTarget?(target: DeliveryTarget, content: unknown
|
|
100
|
+
sendTarget?(target: DeliveryTarget, content: unknown, options?: Readonly<{
|
|
101
|
+
locale?: Locale;
|
|
102
|
+
}>): Promise<string | null>;
|
|
77
103
|
/** Runs related operations against one IM Runtime snapshot captured at ingress. */
|
|
78
104
|
runWithView?<T>(operation: () => Promise<T>): Promise<T>;
|
|
79
105
|
/** Exact operations declared by one live Endpoint. */
|
|
@@ -94,12 +120,16 @@ export interface OutboundHost {
|
|
|
94
120
|
typing?(input: OutboundTypingInput): Promise<void>;
|
|
95
121
|
}
|
|
96
122
|
/** Root-only process port: requester is supplied by the owner-bound facade. */
|
|
97
|
-
export interface OutboundRootHost extends Omit<OutboundHost, 'sendTarget'> {
|
|
98
|
-
|
|
123
|
+
export interface OutboundRootHost extends Omit<OutboundHost, 'sendTarget' | 'captureOperation'> {
|
|
124
|
+
captureOperation?(input: OutboundCaptureInput, requester: PluginId): CapturedOutboundOperation;
|
|
125
|
+
sendTarget?(target: DeliveryTarget, content: unknown, requester: PluginId, options?: Readonly<{
|
|
126
|
+
locale?: Locale;
|
|
127
|
+
}>): Promise<string | null>;
|
|
99
128
|
}
|
|
100
129
|
/** Preserve the sending plugin as requester, independent of the destination endpoint owner. */
|
|
101
130
|
export declare class PluginOutboundHost implements OutboundHost {
|
|
102
131
|
#private;
|
|
132
|
+
readonly captureOperation?: OutboundHost['captureOperation'];
|
|
103
133
|
readonly sendTarget?: OutboundHost['sendTarget'];
|
|
104
134
|
readonly runWithView?: OutboundHost['runWithView'];
|
|
105
135
|
readonly capabilities?: OutboundHost['capabilities'];
|
package/lib/outbound-host.js
CHANGED
|
@@ -3,6 +3,7 @@ import { generationAdmissionBinder, generationAdmissionSource } from './admissio
|
|
|
3
3
|
import { OwnerOperation } from './owner-operation.js';
|
|
4
4
|
/** Preserve the sending plugin as requester, independent of the destination endpoint owner. */
|
|
5
5
|
export class PluginOutboundHost {
|
|
6
|
+
captureOperation;
|
|
6
7
|
sendTarget;
|
|
7
8
|
runWithView;
|
|
8
9
|
capabilities;
|
|
@@ -20,6 +21,8 @@ export class PluginOutboundHost {
|
|
|
20
21
|
this.#host = host;
|
|
21
22
|
this.#admission = admission;
|
|
22
23
|
this.#operation = new OwnerOperation(admission, 'Outbound');
|
|
24
|
+
const captureOperation = host.captureOperation?.bind(host);
|
|
25
|
+
this.captureOperation = captureOperation ? input => this.#run(() => captureOperation(input, this.#owner)) : undefined;
|
|
23
26
|
const sendTarget = host.sendTarget?.bind(host);
|
|
24
27
|
const runWithView = host.runWithView?.bind(host);
|
|
25
28
|
const capabilities = host.capabilities?.bind(host);
|
|
@@ -28,7 +31,9 @@ export class PluginOutboundHost {
|
|
|
28
31
|
const recall = host.recall?.bind(host);
|
|
29
32
|
const edit = host.edit?.bind(host);
|
|
30
33
|
const typing = host.typing?.bind(host);
|
|
31
|
-
this.sendTarget = sendTarget ? async (target, content) => this.#run(() =>
|
|
34
|
+
this.sendTarget = sendTarget ? async (target, content, options) => this.#run(() => options
|
|
35
|
+
? sendTarget(target, content, this.#owner, options)
|
|
36
|
+
: sendTarget(target, content, this.#owner)) : undefined;
|
|
32
37
|
this.runWithView = runWithView ? (operation) => this.#run(() => runWithView(operation)) : undefined;
|
|
33
38
|
this.capabilities = capabilities ? (input) => this.#run(() => capabilities(input)) : undefined;
|
|
34
39
|
this.addReaction = addReaction ? (input) => this.#run(() => addReaction(input)) : undefined;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type GenerationAdmissionSource } from './admission.js';
|
|
2
|
+
import type { PluginId, FeatureId } from './identity.js';
|
|
3
|
+
import type { JsonValue } from './persistent-state.js';
|
|
4
|
+
/**
|
|
5
|
+
* Plugin-registered Console RPC (zhinjs/zhin#970).
|
|
6
|
+
*
|
|
7
|
+
* A plugin publishes one {@link PluginConsoleRpc} through {@link pluginConsoleRpcToken}. The Host
|
|
8
|
+
* exposes it on the existing Console RPC envelope as `plugin.<pluginId>.status` and
|
|
9
|
+
* `plugin.<pluginId>.<action>`, where `<pluginId>` is the owner PluginId (for example
|
|
10
|
+
* `root/typesafe`). No WebSocket, no new HTTP route, and no generic config writes.
|
|
11
|
+
*
|
|
12
|
+
* Authorization follows the Console Page the method is bound to (`page`): the caller must
|
|
13
|
+
* satisfy that page's `requiredPermissions` / `requiredRoles`, plus the method's own. The full
|
|
14
|
+
* (admin) token may call everything. Other tokens (`user` / `demo`) may call a method when they
|
|
15
|
+
* hold every required permission and role. An action is open to them only when it declares its
|
|
16
|
+
* own `requiredPermissions` / `requiredRoles` / `anyOfPermissions` (the page's requirements apply on
|
|
17
|
+
* top, but never grant an action by themselves); an unguarded `status` needs `demo: true`.
|
|
18
|
+
* `anyOfPermissions` is satisfied by holding at least one of the listed permissions (for a new
|
|
19
|
+
* permission that coexists with an older one, e.g. `ops:approve` beside `tool_approvals:resolve`).
|
|
20
|
+
*/
|
|
21
|
+
export type PluginConsoleRpcJson = JsonValue;
|
|
22
|
+
export type PluginConsoleRpcInput = Readonly<Record<string, JsonValue>>;
|
|
23
|
+
export interface PluginConsoleRpcCall {
|
|
24
|
+
/** Aborted when the Host request times out. Long work must observe it. */
|
|
25
|
+
readonly signal: AbortSignal;
|
|
26
|
+
/** Authenticated operation view; no snapshot, configuration, resource or private resolver access. */
|
|
27
|
+
readonly runtime?: Readonly<{
|
|
28
|
+
owner: PluginId;
|
|
29
|
+
generation: number;
|
|
30
|
+
operator: boolean;
|
|
31
|
+
project(feature: FeatureId): unknown;
|
|
32
|
+
}>;
|
|
33
|
+
/** Framework-only request-local notification before a validated durable mutation begins. */
|
|
34
|
+
commitStarted?(): void;
|
|
35
|
+
}
|
|
36
|
+
export interface PluginConsoleRpcMethod<TInput = PluginConsoleRpcInput> {
|
|
37
|
+
/** Local name of a Page owned by the same plugin (`pages/<page>/index.tsx`). Its access rules apply. */
|
|
38
|
+
readonly page?: string;
|
|
39
|
+
/** Extra permissions on top of the page's own requirements. */
|
|
40
|
+
readonly requiredPermissions?: readonly string[];
|
|
41
|
+
/** Extra roles on top of the page's own requirements. */
|
|
42
|
+
readonly requiredRoles?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* The caller must also hold at least one of these (in addition to every `requiredPermissions`).
|
|
45
|
+
* Counts as the method's own requirement for actions.
|
|
46
|
+
*/
|
|
47
|
+
readonly anyOfPermissions?: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Return a small, user-facing JSON shape. Never return raw log records, prompts, chat lines,
|
|
50
|
+
* secrets or exception text: the Console shows this to ordinary users.
|
|
51
|
+
*/
|
|
52
|
+
handler(input: TInput, call: PluginConsoleRpcCall): JsonValue | Promise<JsonValue>;
|
|
53
|
+
}
|
|
54
|
+
export interface PluginConsoleRpcStatus extends PluginConsoleRpcMethod<PluginConsoleRpcInput> {
|
|
55
|
+
/** Allow the demo-scope Console token to read this status. Default `false`. */
|
|
56
|
+
readonly demo?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface PluginConsoleRpcDefinition {
|
|
59
|
+
/** Read-only status for the plugin's Console page(s): `plugin.<pluginId>.status`. */
|
|
60
|
+
readonly status?: PluginConsoleRpcStatus;
|
|
61
|
+
/**
|
|
62
|
+
* Extra read-only views beside `status`: `plugin.<pluginId>.<name>`. Same access rules as
|
|
63
|
+
* `status`; use them when one plugin backs several pages with different permissions.
|
|
64
|
+
*/
|
|
65
|
+
readonly queries?: Readonly<Record<string, PluginConsoleRpcStatus>>;
|
|
66
|
+
/**
|
|
67
|
+
* Limited, named actions: `plugin.<pluginId>.<name>`. Non-admin tokens may run one only when the
|
|
68
|
+
* action declares its own requirements and the token holds them (plus the page's); otherwise
|
|
69
|
+
* admin-only.
|
|
70
|
+
*/
|
|
71
|
+
readonly actions?: Readonly<Record<string, PluginConsoleRpcMethod>>;
|
|
72
|
+
}
|
|
73
|
+
export interface NormalizedPluginConsoleRpcMethod {
|
|
74
|
+
readonly page?: string;
|
|
75
|
+
readonly requiredPermissions: readonly string[];
|
|
76
|
+
readonly requiredRoles: readonly string[];
|
|
77
|
+
/** Empty when the method has no either-of requirement. */
|
|
78
|
+
readonly anyOfPermissions: readonly string[];
|
|
79
|
+
readonly demo: boolean;
|
|
80
|
+
/** `status` and `queries` are read-only; actions are not. */
|
|
81
|
+
readonly readOnly: boolean;
|
|
82
|
+
readonly handler: PluginConsoleRpcMethod['handler'];
|
|
83
|
+
}
|
|
84
|
+
/** Value published under {@link pluginConsoleRpcToken}; create it with {@link definePluginConsoleRpc}. */
|
|
85
|
+
export interface PluginConsoleRpc extends GenerationAdmissionSource {
|
|
86
|
+
readonly methods: ReadonlyMap<string, NormalizedPluginConsoleRpcMethod>;
|
|
87
|
+
}
|
|
88
|
+
/** Safe, user-facing failure. `message` is shown in the Console; never put raw errors in it. */
|
|
89
|
+
export declare class PluginConsoleRpcError extends Error {
|
|
90
|
+
readonly code: 'invalid_input' | 'conflict' | 'not_found' | 'unsupported' | 'unavailable' | 'forbidden' | 'commit_unknown';
|
|
91
|
+
constructor(code: PluginConsoleRpcError['code'], message: string);
|
|
92
|
+
}
|
|
93
|
+
export declare const pluginConsoleRpcToken: import("./token.js").Token<PluginConsoleRpc>;
|
|
94
|
+
export declare const PLUGIN_CONSOLE_RPC_STATUS = "status";
|
|
95
|
+
/** Validates and freezes a plugin's Console RPC surface. Throws on invalid names at setup time. */
|
|
96
|
+
export declare function definePluginConsoleRpc(definition: PluginConsoleRpcDefinition): PluginConsoleRpc;
|