@zhin.js/plugin-runtime 1.1.1 → 1.1.3

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.
@@ -1,15 +1,23 @@
1
+ import { type PluginId } from './identity.js';
1
2
  export interface DatabaseHostSelection {
2
3
  where(query: Record<string, unknown>): DatabaseHostSelection;
3
4
  orderBy(field: string, direction?: 'ASC' | 'DESC'): DatabaseHostSelection;
4
5
  limit(count: number): DatabaseHostSelection;
5
6
  then<TResult1 = Record<string, unknown>[], TResult2 = never>(onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
6
7
  }
8
+ /**
9
+ * select() 需要显式列名:'*' 在 SQL 方言里会被当成字面列名(no such column: "*"),
10
+ * 类型层直接拒绝;运行时另有可读报错兜底(见 Host 实现)。
11
+ */
12
+ type SelectFields<Fields extends readonly string[]> = {
13
+ [K in keyof Fields]: Fields[K] extends '*' ? never : Fields[K];
14
+ };
7
15
  /**
8
16
  * Minimal model surface shared by lottery / rss / group-suite memory stores
9
17
  * and the Host-backed `@zhin.js/database` adapter.
10
18
  */
11
19
  export interface DatabaseHostModel {
12
- select(...fields: string[]): DatabaseHostSelection;
20
+ select<Fields extends string[]>(...fields: SelectFields<Fields>): DatabaseHostSelection;
13
21
  insert(row: Record<string, unknown>): Promise<unknown>;
14
22
  delete(): {
15
23
  where(query: Record<string, unknown>): Promise<unknown>;
@@ -83,4 +91,44 @@ export interface DatabaseHost {
83
91
  start(): Promise<void>;
84
92
  stop(): Promise<void>;
85
93
  }
86
- export declare const databaseHostToken: import("./token.js").Token<DatabaseHost>;
94
+ /**
95
+ * Persistence surface visible to a plugin. Names are logical to the owner;
96
+ * the Runtime maps them to private physical table names before they reach the
97
+ * process-wide DatabaseHost.
98
+ */
99
+ export interface PluginDatabaseHost {
100
+ readonly owner: PluginId;
101
+ readonly dialect: string;
102
+ readonly started: boolean;
103
+ define(name: string, definition: Record<string, unknown>): void;
104
+ tables(): readonly string[];
105
+ models: {
106
+ get(name: string): DatabaseHostModel | undefined;
107
+ };
108
+ }
109
+ /**
110
+ * Maps a plugin's logical resource name to its process-wide physical name.
111
+ * Root keeps its historical bare names so existing projects do not need a
112
+ * database migration merely to adopt scoped child plugins.
113
+ */
114
+ export declare function qualifyPluginResourceName(owner: PluginId, name: string): string;
115
+ /** Reverse `qualifyPluginResourceName` only when the name belongs to owner. */
116
+ export declare function unqualifyPluginResourceName(owner: PluginId, name: string): string | undefined;
117
+ /** Creates a tenant facade without exposing Console administration or raw DB access. */
118
+ export declare function createPluginDatabaseHost(owner: PluginId, host: DatabaseHost): PluginDatabaseHost;
119
+ /**
120
+ * Recovers a process host from the standard root token or a scoped facade.
121
+ * This keeps custom RootRuntime installers written against the pre-facade
122
+ * token working while child scopes begin receiving tenant boundaries.
123
+ */
124
+ export declare function unwrapPluginDatabaseHost(host: DatabaseHost | PluginDatabaseHost): DatabaseHost | undefined;
125
+ /**
126
+ * Plugin-facing scoped persistence token. Runtime replaces this binding for
127
+ * every Plugin Scope; plugin code must never receive the process-wide host.
128
+ * The owner encoding keeps `root/a` and `root/a/b` disjoint, so a parent
129
+ * facade cannot accidentally enumerate a descendant's tables.
130
+ */
131
+ export declare const databaseHostToken: import("./token.js").Token<PluginDatabaseHost>;
132
+ /** Root-only process host for CLI composition and Console administration. */
133
+ export declare const databaseRootHostToken: import("./token.js").Token<DatabaseHost>;
134
+ export {};
@@ -1,2 +1,71 @@
1
+ import { pluginOwnerResourceKey, rootPluginId } from './identity.js';
1
2
  import { createToken } from './token.js';
2
- export const databaseHostToken = createToken('zhin.database.host', 'Plugin Runtime database host');
3
+ const resourcePrefix = '__zhin_plugin__';
4
+ const resourceSeparator = '__';
5
+ const roots = new WeakMap();
6
+ /**
7
+ * Maps a plugin's logical resource name to its process-wide physical name.
8
+ * Root keeps its historical bare names so existing projects do not need a
9
+ * database migration merely to adopt scoped child plugins.
10
+ */
11
+ export function qualifyPluginResourceName(owner, name) {
12
+ assertLogicalResourceName(name);
13
+ if (owner === rootPluginId())
14
+ return name;
15
+ return `${resourcePrefix}${pluginOwnerResourceKey(owner)}${resourceSeparator}${name}`;
16
+ }
17
+ /** Reverse `qualifyPluginResourceName` only when the name belongs to owner. */
18
+ export function unqualifyPluginResourceName(owner, name) {
19
+ if (owner === rootPluginId())
20
+ return name.startsWith(resourcePrefix) ? undefined : name;
21
+ const prefix = `${resourcePrefix}${pluginOwnerResourceKey(owner)}${resourceSeparator}`;
22
+ return name.startsWith(prefix) ? name.slice(prefix.length) : undefined;
23
+ }
24
+ /** Creates a tenant facade without exposing Console administration or raw DB access. */
25
+ export function createPluginDatabaseHost(owner, host) {
26
+ const facade = Object.freeze({
27
+ owner,
28
+ get dialect() { return host.dialect; },
29
+ get started() { return host.started; },
30
+ define(name, definition) {
31
+ host.define(qualifyPluginResourceName(owner, name), definition);
32
+ },
33
+ tables() {
34
+ return Object.freeze(host.tables().flatMap((name) => {
35
+ const logical = unqualifyPluginResourceName(owner, name);
36
+ return logical === undefined ? [] : [logical];
37
+ }));
38
+ },
39
+ models: Object.freeze({
40
+ get(name) {
41
+ return host.models.get(qualifyPluginResourceName(owner, name));
42
+ },
43
+ }),
44
+ });
45
+ roots.set(facade, host);
46
+ return facade;
47
+ }
48
+ /**
49
+ * Recovers a process host from the standard root token or a scoped facade.
50
+ * This keeps custom RootRuntime installers written against the pre-facade
51
+ * token working while child scopes begin receiving tenant boundaries.
52
+ */
53
+ export function unwrapPluginDatabaseHost(host) {
54
+ if ('getRawDatabase' in host)
55
+ return host;
56
+ return roots.get(host);
57
+ }
58
+ function assertLogicalResourceName(name) {
59
+ if (!name || name.startsWith(resourcePrefix)) {
60
+ throw new TypeError(`Invalid plugin resource name: ${name}`);
61
+ }
62
+ }
63
+ /**
64
+ * Plugin-facing scoped persistence token. Runtime replaces this binding for
65
+ * every Plugin Scope; plugin code must never receive the process-wide host.
66
+ * The owner encoding keeps `root/a` and `root/a/b` disjoint, so a parent
67
+ * facade cannot accidentally enumerate a descendant's tables.
68
+ */
69
+ export const databaseHostToken = createToken('zhin.database.host', 'Plugin Runtime scoped database host');
70
+ /** Root-only process host for CLI composition and Console administration. */
71
+ export const databaseRootHostToken = createToken('zhin.database.root-host', 'Plugin Runtime root database host');
package/lib/identity.d.ts CHANGED
@@ -18,5 +18,13 @@ export declare function tokenId(value: string): TokenId;
18
18
  export declare function featureId(value: string): FeatureId;
19
19
  export declare function rootPluginId(): PluginId;
20
20
  export declare function childPluginId(parent: PluginId, instanceKey: string): PluginId;
21
+ /**
22
+ * Database-safe owner encoding used by process-wide plugin resources.
23
+ *
24
+ * Each path segment is length-prefixed and the final underscore is followed
25
+ * by a resource separator. A parent owner therefore cannot match a child
26
+ * owner merely because their PluginId has a common path prefix.
27
+ */
28
+ export declare function pluginOwnerResourceKey(owner: PluginId): string;
21
29
  export declare function capabilityId(owner: PluginId, feature: FeatureId, localName: string): CapabilityId;
22
30
  export {};
package/lib/identity.js CHANGED
@@ -24,6 +24,19 @@ export function childPluginId(parent, instanceKey) {
24
24
  }
25
25
  return `${parent}/${instanceKey}`;
26
26
  }
27
+ /**
28
+ * Database-safe owner encoding used by process-wide plugin resources.
29
+ *
30
+ * Each path segment is length-prefixed and the final underscore is followed
31
+ * by a resource separator. A parent owner therefore cannot match a child
32
+ * owner merely because their PluginId has a common path prefix.
33
+ */
34
+ export function pluginOwnerResourceKey(owner) {
35
+ return `${String(owner)
36
+ .split('/')
37
+ .map((segment) => `${segment.length}_${segment}`)
38
+ .join('_')}_`;
39
+ }
27
40
  export function capabilityId(owner, feature, localName) {
28
41
  if (!capabilityLocalNamePattern.test(localName)) {
29
42
  throw new TypeError(`Invalid capability local name: ${localName}`);
@@ -1,3 +1,17 @@
1
+ /**
2
+ * 未锚定 endpoint 的会话地址(结构对齐 `@zhin.js/im-contract` 的
3
+ * `Omit<ConversationRef, 'endpoint'>`;plugin-runtime 是零依赖 kernel 层,
4
+ * 不能 import IM 契约,故此处独立声明,靠结构类型兼容)。
5
+ */
6
+ export interface OutboundConversation {
7
+ readonly kind: 'private' | 'group' | 'channel';
8
+ readonly id: string;
9
+ readonly parent?: Readonly<{
10
+ readonly kind: 'group' | 'channel';
11
+ readonly id: string;
12
+ }>;
13
+ readonly threadId?: string;
14
+ }
1
15
  /**
2
16
  * Thin Host Resource for proactive Adapter outbound (RSS poll, lottery push,
3
17
  * activity-feedback typing text). Implementations typically wrap ImRuntime
@@ -6,8 +20,7 @@
6
20
  export interface OutboundSendInput {
7
21
  readonly adapter: string;
8
22
  readonly endpointId: string;
9
- readonly channelType: string;
10
- readonly channelId: string;
23
+ readonly conversation: OutboundConversation;
11
24
  readonly content: string;
12
25
  }
13
26
  export interface OutboundReactionInput {
@@ -1,9 +1,12 @@
1
+ import { type PluginId } from './identity.js';
1
2
  /**
2
3
  * Thin Host Resource for Plugin Runtime cron jobs.
3
4
  * Implementations typically wrap `@zhin.js/schedule` CalendarScheduler (solar cron).
4
5
  */
5
6
  export interface ScheduleJobRegistration {
6
7
  readonly id: string;
8
+ /** Runtime-assigned owner for diagnostics and Console inventory. */
9
+ readonly owner?: PluginId;
7
10
  /** 6-field solar cron: `秒 分 时 日 月 周` */
8
11
  readonly cron: string;
9
12
  readonly description?: string;
@@ -16,6 +19,25 @@ export interface ScheduleHost {
16
19
  readonly id: string;
17
20
  readonly cron: string;
18
21
  readonly description?: string;
22
+ readonly owner?: PluginId;
19
23
  }[];
20
24
  }
21
- export declare const scheduleHostToken: import("./token.js").Token<ScheduleHost>;
25
+ /** Scoped scheduling surface exposed to a Plugin setup function. */
26
+ export interface PluginScheduleHost {
27
+ readonly owner: PluginId;
28
+ register(job: ScheduleJobRegistration): () => void;
29
+ list(): readonly {
30
+ readonly id: string;
31
+ readonly cron: string;
32
+ readonly description?: string;
33
+ }[];
34
+ }
35
+ export declare function qualifyPluginScheduleId(owner: PluginId, id: string): string;
36
+ export declare function unqualifyPluginScheduleId(owner: PluginId, id: string): string | undefined;
37
+ export declare function createPluginScheduleHost(owner: PluginId, host: ScheduleHost): PluginScheduleHost;
38
+ /** See unwrapPluginDatabaseHost: preserves legacy custom RootResource installers. */
39
+ export declare function unwrapPluginScheduleHost(host: ScheduleHost | PluginScheduleHost): ScheduleHost;
40
+ /** Plugin-facing token; Runtime injects a facade bound to the current owner. */
41
+ export declare const scheduleHostToken: import("./token.js").Token<PluginScheduleHost>;
42
+ /** Root-only process scheduler for CLI composition and Console administration. */
43
+ export declare const scheduleRootHostToken: import("./token.js").Token<ScheduleHost>;
@@ -1,2 +1,52 @@
1
+ import { pluginOwnerResourceKey, rootPluginId } from './identity.js';
1
2
  import { createToken } from './token.js';
2
- export const scheduleHostToken = createToken('zhin.schedule.host', 'Plugin Runtime solar cron host');
3
+ const jobPrefix = '__zhin_plugin__';
4
+ const jobSeparator = '__';
5
+ const roots = new WeakMap();
6
+ export function qualifyPluginScheduleId(owner, id) {
7
+ assertLogicalJobId(id);
8
+ if (owner === rootPluginId())
9
+ return id;
10
+ return `${jobPrefix}${pluginOwnerResourceKey(owner)}${jobSeparator}${id}`;
11
+ }
12
+ export function unqualifyPluginScheduleId(owner, id) {
13
+ if (owner === rootPluginId())
14
+ return id.startsWith(jobPrefix) ? undefined : id;
15
+ const prefix = `${jobPrefix}${pluginOwnerResourceKey(owner)}${jobSeparator}`;
16
+ return id.startsWith(prefix) ? id.slice(prefix.length) : undefined;
17
+ }
18
+ export function createPluginScheduleHost(owner, host) {
19
+ const facade = Object.freeze({
20
+ owner,
21
+ register(job) {
22
+ return host.register({ ...job, id: qualifyPluginScheduleId(owner, job.id), owner });
23
+ },
24
+ list() {
25
+ return Object.freeze(host.list().flatMap((job) => {
26
+ const id = unqualifyPluginScheduleId(owner, job.id);
27
+ if (id === undefined)
28
+ return [];
29
+ return [Object.freeze({
30
+ id,
31
+ cron: job.cron,
32
+ ...(job.description === undefined ? {} : { description: job.description }),
33
+ })];
34
+ }));
35
+ },
36
+ });
37
+ roots.set(facade, host);
38
+ return facade;
39
+ }
40
+ /** See unwrapPluginDatabaseHost: preserves legacy custom RootResource installers. */
41
+ export function unwrapPluginScheduleHost(host) {
42
+ return roots.get(host) ?? host;
43
+ }
44
+ function assertLogicalJobId(id) {
45
+ if (!id || id.startsWith(jobPrefix)) {
46
+ throw new TypeError(`Invalid plugin schedule id: ${id}`);
47
+ }
48
+ }
49
+ /** Plugin-facing token; Runtime injects a facade bound to the current owner. */
50
+ export const scheduleHostToken = createToken('zhin.schedule.host', 'Plugin Runtime scoped solar cron host');
51
+ /** Root-only process scheduler for CLI composition and Console administration. */
52
+ export const scheduleRootHostToken = createToken('zhin.schedule.root-host', 'Plugin Runtime root solar cron host');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -16,7 +16,7 @@
16
16
  "lib"
17
17
  ],
18
18
  "devDependencies": {
19
- "@types/node": "^26.1.0",
19
+ "@types/node": "^26.1.2",
20
20
  "typescript": "^6.0.3"
21
21
  },
22
22
  "engines": {