@zhin.js/plugin-runtime 1.1.19 → 1.1.21

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.
@@ -0,0 +1,5 @@
1
+ export interface PluginConfigUpdateDeclaration {
2
+ readonly featurePaths: readonly (readonly string[])[];
3
+ }
4
+ /** @internal Normalize inert ownership metadata at authoring and snapshot boundaries. */
5
+ export declare function freezePluginConfigUpdate(value: PluginConfigUpdateDeclaration): PluginConfigUpdateDeclaration;
@@ -0,0 +1,15 @@
1
+ /** @internal Normalize inert ownership metadata at authoring and snapshot boundaries. */
2
+ export function freezePluginConfigUpdate(value) {
3
+ if (!value || typeof value !== 'object' || Object.keys(value).some(key => key !== 'featurePaths') || !Array.isArray(value.featurePaths)) {
4
+ throw new TypeError('Plugin configUpdate must declare featurePaths');
5
+ }
6
+ return Object.freeze({ featurePaths: Object.freeze(Array.from(value.featurePaths, (path) => {
7
+ if (!Array.isArray(path) || !path.length)
8
+ throw new TypeError('Plugin configUpdate paths must be safe nonempty paths');
9
+ const parts = Array.from(path);
10
+ if (parts.some(part => typeof part !== 'string' || !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
11
+ throw new TypeError('Plugin configUpdate paths must be safe nonempty paths');
12
+ }
13
+ return Object.freeze(parts);
14
+ })) });
15
+ }
@@ -47,8 +47,29 @@ export interface OutboundEndpointInput {
47
47
  readonly adapter: string;
48
48
  readonly endpointKey: string;
49
49
  }
50
+ /**
51
+ * Per-conversation-kind view of an Endpoint's capability profile (ADR 0044). Structurally the same as
52
+ * `EndpointKindCapabilities` in `@zhin.js/im-contract`; a missing kind or field means "not supported".
53
+ */
54
+ export interface OutboundKindCapabilities {
55
+ readonly feedback?: Readonly<{
56
+ mode: 'typing' | 'reaction' | 'status-message' | 'none';
57
+ emoji?: string;
58
+ text?: string;
59
+ refreshSeconds?: number;
60
+ }>;
61
+ readonly reaction: boolean;
62
+ readonly edit: boolean;
63
+ readonly recall: boolean;
64
+ readonly spool: boolean;
65
+ /** Messages here must quote an inbound message. */
66
+ readonly needsReply: boolean;
67
+ readonly groupInfo: boolean;
68
+ readonly memberProfile: boolean;
69
+ }
50
70
  export interface OutboundEndpointCapabilities {
51
71
  readonly operations: readonly OutboundEndpointOperation[];
72
+ readonly kinds?: Readonly<Partial<Record<'private' | 'group' | 'channel', OutboundKindCapabilities>>>;
52
73
  }
53
74
  export interface OutboundEditInput {
54
75
  readonly message: OutboundMessage;
@@ -20,9 +20,11 @@ import type { JsonValue } from './persistent-state.js';
20
20
  */
21
21
  export type PluginConsoleRpcJson = JsonValue;
22
22
  export type PluginConsoleRpcInput = Readonly<Record<string, JsonValue>>;
23
- export interface PluginConsoleRpcCall {
23
+ export interface PluginConsoleRpcCall<TConfig = Record<string, unknown>> {
24
24
  /** Aborted when the Host request times out. Long work must observe it. */
25
25
  readonly signal: AbortSignal;
26
+ /** Validated plugin configuration for this generation, when available. */
27
+ readonly config?: TConfig;
26
28
  /** Authenticated operation view; no snapshot, configuration, resource or private resolver access. */
27
29
  readonly runtime?: Readonly<{
28
30
  owner: PluginId;
@@ -33,7 +35,10 @@ export interface PluginConsoleRpcCall {
33
35
  /** Framework-only request-local notification before a validated durable mutation begins. */
34
36
  commitStarted?(): void;
35
37
  }
36
- export interface PluginConsoleRpcMethod<TInput = PluginConsoleRpcInput> {
38
+ export type PluginConsoleRpcCallContext<TMethods = Record<string, unknown>, TKey extends keyof TMethods = keyof TMethods, TConfig = Record<string, unknown>> = PluginConsoleRpcCall<TConfig> & {
39
+ readonly method?: TKey;
40
+ };
41
+ export interface PluginConsoleRpcMethod<TInput = PluginConsoleRpcInput, TConfig = Record<string, unknown>> {
37
42
  /** Local name of a Page owned by the same plugin (`pages/<page>/index.tsx`). Its access rules apply. */
38
43
  readonly page?: string;
39
44
  /** Extra permissions on top of the page's own requirements. */
@@ -49,26 +54,29 @@ export interface PluginConsoleRpcMethod<TInput = PluginConsoleRpcInput> {
49
54
  * Return a small, user-facing JSON shape. Never return raw log records, prompts, chat lines,
50
55
  * secrets or exception text: the Console shows this to ordinary users.
51
56
  */
52
- handler(input: TInput, call: PluginConsoleRpcCall): JsonValue | Promise<JsonValue>;
57
+ handler(input: TInput, call: PluginConsoleRpcCall<TConfig>): JsonValue | Promise<JsonValue>;
53
58
  }
54
- export interface PluginConsoleRpcStatus extends PluginConsoleRpcMethod<PluginConsoleRpcInput> {
59
+ export interface PluginConsoleRpcStatus<TConfig = Record<string, unknown>> extends PluginConsoleRpcMethod<PluginConsoleRpcInput, TConfig> {
55
60
  /** Allow the demo-scope Console token to read this status. Default `false`. */
56
61
  readonly demo?: boolean;
57
62
  }
58
- export interface PluginConsoleRpcDefinition {
63
+ export interface PluginConsoleRpcDefinition<TConfig = Record<string, unknown>> {
59
64
  /** Read-only status for the plugin's Console page(s): `plugin.<pluginId>.status`. */
60
- readonly status?: PluginConsoleRpcStatus;
65
+ readonly status?: PluginConsoleRpcStatus<TConfig>;
61
66
  /**
62
67
  * Extra read-only views beside `status`: `plugin.<pluginId>.<name>`. Same access rules as
63
68
  * `status`; use them when one plugin backs several pages with different permissions.
64
69
  */
65
- readonly queries?: Readonly<Record<string, PluginConsoleRpcStatus>>;
70
+ readonly queries?: Readonly<Record<string, PluginConsoleRpcStatus<TConfig>>>;
66
71
  /**
67
72
  * Limited, named actions: `plugin.<pluginId>.<name>`. Non-admin tokens may run one only when the
68
73
  * action declares its own requirements and the token holds them (plus the page's); otherwise
69
74
  * admin-only.
70
75
  */
71
- readonly actions?: Readonly<Record<string, PluginConsoleRpcMethod>>;
76
+ readonly actions?: Readonly<Record<string, PluginConsoleRpcMethod<PluginConsoleRpcInput, TConfig>>>;
77
+ }
78
+ export interface PluginConsoleRpcDefiner<TConfig = Record<string, unknown>> {
79
+ (definition: PluginConsoleRpcDefinition<TConfig>): PluginConsoleRpc;
72
80
  }
73
81
  export interface NormalizedPluginConsoleRpcMethod {
74
82
  readonly page?: string;
@@ -93,4 +101,7 @@ export declare class PluginConsoleRpcError extends Error {
93
101
  export declare const pluginConsoleRpcToken: import("./token.js").Token<PluginConsoleRpc>;
94
102
  export declare const PLUGIN_CONSOLE_RPC_STATUS = "status";
95
103
  /** Validates and freezes a plugin's Console RPC surface. Throws on invalid names at setup time. */
96
- export declare function definePluginConsoleRpc(definition: PluginConsoleRpcDefinition): PluginConsoleRpc;
104
+ export declare function definePluginConsoleRpc<TConfig = Record<string, unknown>>(definition: PluginConsoleRpcDefinition<TConfig>): PluginConsoleRpc;
105
+ export declare namespace definePluginConsoleRpc {
106
+ var withConfig: <TConfig = Record<string, unknown>>() => PluginConsoleRpcDefiner<TConfig>;
107
+ }
@@ -42,6 +42,13 @@ export function definePluginConsoleRpc(definition) {
42
42
  get [generationAdmissionSource]() { return gates; },
43
43
  });
44
44
  }
45
+ /**
46
+ * Fix the plugin config type first, then define the Console RPC surface:
47
+ * `definePluginConsoleRpc.withConfig<MyConfig>()({ status: { handler(_input, call) { ... } } })`
48
+ */
49
+ definePluginConsoleRpc.withConfig = function withConfig() {
50
+ return (definition) => definePluginConsoleRpc(definition);
51
+ };
45
52
  function normalizeMethod(name, method, demo, readOnly) {
46
53
  if (!method || typeof method.handler !== 'function') {
47
54
  throw new TypeError(`Plugin Console RPC ${name} must have a handler`);
package/lib/plugin.d.ts CHANGED
@@ -7,6 +7,9 @@ import type { GenerationHandoffRegistry } from './handoff.js';
7
7
  import type { FeatureId, PluginId } from './identity.js';
8
8
  import type { Scope, Token } from './token.js';
9
9
  import type { PluginEventSubscriptions } from './plugin-events.js';
10
+ import { type PluginConfigUpdateDeclaration } from './config-update-declaration.js';
11
+ export { freezePluginConfigUpdate } from './config-update-declaration.js';
12
+ export type { PluginConfigUpdateDeclaration } from './config-update-declaration.js';
10
13
  export interface PluginMetadata {
11
14
  readonly displayName?: string;
12
15
  readonly icon?: string;
@@ -48,6 +51,8 @@ export interface PluginDefinition<TConfig = unknown> {
48
51
  readonly config?: PluginConfigSchema<TConfig>;
49
52
  readonly metadata?: PluginMetadata;
50
53
  readonly requires?: readonly Token<unknown>[];
54
+ /** Fields owned by Features; setup must not capture these as mutable business configuration. */
55
+ readonly configUpdate?: PluginConfigUpdateDeclaration;
51
56
  setup?(context: PluginSetupContext<TConfig>): void | Dispose | Promise<void | Dispose>;
52
57
  }
53
58
  /**
package/lib/plugin.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { freezePluginConfigUpdate } from './config-update-declaration.js';
2
+ export { freezePluginConfigUpdate } from './config-update-declaration.js';
1
3
  const pluginNamePattern = /^[a-z][a-z0-9-]*$/;
2
4
  /**
3
5
  * Define the canonical `plugin.ts` entry point.
@@ -28,6 +30,7 @@ export function definePlugin(definition) {
28
30
  ...definition,
29
31
  metadata: definition.metadata ? Object.freeze({ ...definition.metadata }) : undefined,
30
32
  requires: Object.freeze([...(definition.requires ?? [])]),
33
+ ...(definition.configUpdate !== undefined ? { configUpdate: freezePluginConfigUpdate(definition.configUpdate) } : {}),
31
34
  });
32
35
  }
33
36
  // The application facade exposes these Host contracts beside definePlugin.
package/lib/snapshot.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { Dispose } from './dispose.js';
4
4
  import type { GenerationHandoff } from './handoff.js';
5
5
  import type { CapabilityId, FeatureId, PluginId, TokenId } from './identity.js';
6
6
  import type { PluginMetadata } from './plugin.js';
7
+ import { type PluginConfigUpdateDeclaration } from './config-update-declaration.js';
7
8
  export interface PluginNodeSnapshot {
8
9
  readonly id: PluginId;
9
10
  readonly instanceKey: string;
@@ -16,6 +17,7 @@ export interface PluginNodeSnapshot {
16
17
  readonly metadata?: PluginMetadata;
17
18
  /** Immutable schema declaration belonging to this committed generation. */
18
19
  readonly configSchema?: Readonly<Record<string, unknown>>;
20
+ readonly configUpdate?: PluginConfigUpdateDeclaration;
19
21
  }
20
22
  export interface RuntimeSnapshot {
21
23
  readonly generation: number;
package/lib/snapshot.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { collectGenerationAdmissions, replaceGenerationAdmissions, } from './admission.js';
2
+ import { freezePluginConfigUpdate } from './config-update-declaration.js';
2
3
  export class GenerationConflictError extends Error {
3
4
  expected;
4
5
  actual;
@@ -214,8 +215,9 @@ export function createSnapshotView(generation, state) {
214
215
  return Object.freeze({
215
216
  ...state,
216
217
  generation,
217
- tree: readonlyMap([...state.tree].map(([owner, node]) => [owner, node.configSchema
218
- ? Object.freeze({ ...node, configSchema: freezeSchema(structuredClone(node.configSchema)) })
218
+ tree: readonlyMap([...state.tree].map(([owner, node]) => [owner, node.configSchema || node.configUpdate
219
+ ? Object.freeze({ ...node, ...(node.configSchema ? { configSchema: freezeSchema(structuredClone(node.configSchema)) } : {}),
220
+ ...(node.configUpdate ? { configUpdate: freezePluginConfigUpdate(node.configUpdate) } : {}) })
219
221
  : node])),
220
222
  config: readonlyMap(state.config),
221
223
  resources: readonlyMap([...state.resources].map(([owner, resources]) => [owner, readonlyMap(resources)])),
package/lib/token.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface Token<T> {
7
7
  }
8
8
  export declare function createToken<T>(id: string, description?: string): Token<T>;
9
9
  export declare function createRootPrivateToken<T>(id: string, description?: string): Token<T>;
10
+ /** @internal Verify token identity before accepting a generation-private binding. */
11
+ export declare function isRootPrivateToken(token: Token<unknown>): boolean;
10
12
  /** A Root-private capability readable only while projecting its owning Feature. */
11
13
  export declare function createFeaturePrivateToken<T>(id: string, feature: FeatureId, description?: string): Token<T>;
12
14
  /** @internal The actual candidate Root Scope never enters the Feature context. */
package/lib/token.js CHANGED
@@ -10,6 +10,8 @@ export function createRootPrivateToken(id, description) {
10
10
  rootPrivateTokens.add(token);
11
11
  return Object.freeze(token);
12
12
  }
13
+ /** @internal Verify token identity before accepting a generation-private binding. */
14
+ export function isRootPrivateToken(token) { return rootPrivateTokens.has(token); }
13
15
  /** A Root-private capability readable only while projecting its owning Feature. */
14
16
  export function createFeaturePrivateToken(id, feature, description) {
15
17
  const token = createRootPrivateToken(id, description);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-runtime",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "description": "Zero-dependency Plugin tree, Scope and atomic generation runtime for Zhin",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",