@oktis-works/plugin-runtime 0.6.1 → 0.7.0
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/dist/bootstrap.d.ts +86 -21
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +231 -42
- package/dist/bootstrap.js.map +1 -1
- package/dist/content-query.d.ts +29 -0
- package/dist/content-query.d.ts.map +1 -0
- package/dist/content-query.js +109 -0
- package/dist/content-query.js.map +1 -0
- package/dist/hook-registry.d.ts +21 -21
- package/dist/hook-registry.d.ts.map +1 -1
- package/dist/hook-registry.js +26 -26
- package/dist/hook-registry.js.map +1 -1
- package/dist/http.d.ts +49 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +313 -0
- package/dist/http.js.map +1 -0
- package/dist/i18n.d.ts +16 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +73 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.js +3 -3
- package/dist/loader.js.map +1 -1
- package/dist/manifest.d.ts +11 -6
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +227 -17
- package/dist/manifest.js.map +1 -1
- package/dist/plugin-api.d.ts +184 -39
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/plugin-api.js +553 -11
- package/dist/plugin-api.js.map +1 -1
- package/dist/plugin-migrations.d.ts +15 -0
- package/dist/plugin-migrations.d.ts.map +1 -0
- package/dist/plugin-migrations.js +68 -0
- package/dist/plugin-migrations.js.map +1 -0
- package/dist/sandbox.d.ts +13 -11
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +11 -9
- package/dist/sandbox.js.map +1 -1
- package/dist/style-isolation.d.ts +3 -3
- package/dist/style-isolation.js +6 -6
- package/dist/style-isolation.js.map +1 -1
- package/package.json +6 -5
package/dist/bootstrap.d.ts
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
|
+
import { type PluginJobHandler, type AdminExtensionsState, type PluginBlockManifest, type PluginPatternManifest, type EnqueuedAsset, type PluginHealthCheck } from '@oktis-works/plugin-sdk';
|
|
1
2
|
import { type HookRegistry } from './hook-registry.js';
|
|
2
3
|
import type { PluginApiDependencies, RouteDefinition } from './plugin-api.js';
|
|
3
4
|
export interface BootstrapPluginsSql {
|
|
4
5
|
unsafe(text: string, params?: unknown[]): Promise<unknown>;
|
|
5
6
|
}
|
|
6
7
|
export interface BootstrapPluginsOptions {
|
|
7
|
-
/**
|
|
8
|
+
/** Plugins directory (the caller resolves it — plugin-runtime doesn't know the core). */
|
|
8
9
|
pluginsDir: string;
|
|
9
|
-
/**
|
|
10
|
+
/** Postgres client to read enabled plugins; omitted = unknown state (no-op). */
|
|
10
11
|
sql?: BootstrapPluginsSql;
|
|
11
|
-
/**
|
|
12
|
+
/** Target registry (default: the scope registry — platform or `tenantId`). */
|
|
12
13
|
registry?: HookRegistry;
|
|
13
|
-
/** Statuses
|
|
14
|
+
/** Statuses considered enabled (default: ACTIVATED, RUNNING). */
|
|
14
15
|
enabledStatuses?: string[];
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
* string → tenant (`plugin_tenants`
|
|
17
|
+
* Load scope: `null`/omitted → platform (`plugins.tenant_id IS NULL`);
|
|
18
|
+
* string → tenant (`plugin_tenants` with ENABLED status).
|
|
18
19
|
*/
|
|
19
20
|
tenantId?: string | null;
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
22
|
+
* Granted permission policy (allowlist). Default: the full CMS permission
|
|
23
|
+
* catalog. Never filled with the manifest's own permissions.
|
|
23
24
|
*/
|
|
24
25
|
permissionPolicy?: string[];
|
|
25
|
-
/**
|
|
26
|
+
/** Hook callback timeout in ms (0 = no timeout). Default 5000. */
|
|
26
27
|
hookTimeoutMs?: number;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Real dependencies for the PluginAPI (settings, eventBus, db, etc.).
|
|
30
|
+
* When omitted, only the legacy Hooks API is used (compatibility).
|
|
31
|
+
* The core injects this with real services.
|
|
31
32
|
*/
|
|
32
33
|
apiDeps?: Partial<PluginApiDependencies>;
|
|
33
34
|
}
|
|
34
35
|
export interface PluginBootstrapResult {
|
|
35
36
|
/** Nomes habilitados no banco (vazio quando o banco falhou = estado desconhecido). */
|
|
36
37
|
enabled: string[];
|
|
37
|
-
/**
|
|
38
|
+
/** Loaded in this sync. */
|
|
38
39
|
loaded: string[];
|
|
39
|
-
/** Hooks
|
|
40
|
+
/** Hooks removed in this sync (left the enabled list). */
|
|
40
41
|
unloaded: string[];
|
|
41
42
|
/** Falhas isoladas por plugin (nunca abortam o lote). */
|
|
42
43
|
failed: Array<{
|
|
@@ -44,31 +45,95 @@ export interface PluginBootstrapResult {
|
|
|
44
45
|
error: string;
|
|
45
46
|
}>;
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
+
/** Scope with registered loads (for inspection/runtime state). */
|
|
48
49
|
export declare function listLoadedScopes(): Array<{
|
|
49
50
|
scope: string;
|
|
50
51
|
registry: HookRegistry;
|
|
51
52
|
plugins: string[];
|
|
52
53
|
}>;
|
|
53
|
-
/**
|
|
54
|
+
/** Gets the routes registered by plugins in a scope. */
|
|
54
55
|
export declare function getLoadedPluginRoutes(scopeKey: string): Array<{
|
|
55
56
|
pluginName: string;
|
|
56
57
|
routes: RouteDefinition[];
|
|
58
|
+
/** Route rate-limit rpm (manifest rateLimit.rpm, Item 17). */
|
|
59
|
+
rpm?: number;
|
|
57
60
|
}>;
|
|
58
61
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
+
* Handlers de jobs dos plugins em um escopo (dispatch pelo worker:
|
|
63
|
+
* `plugin:{tenant}:{plugin}:{jobName}` → handler).
|
|
64
|
+
*/
|
|
65
|
+
export declare function getLoadedPluginJobHandlers(scopeKey: string): Array<{
|
|
66
|
+
pluginName: string;
|
|
67
|
+
handlers: Map<string, PluginJobHandler>;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Runtime `api.admin` state of the plugins loaded in a scope (slots
|
|
71
|
+
* registered at runtime) — consumed by GET /api/v1/plugins/extensions, which
|
|
72
|
+
* merges it with `manifest.admin`. Only states with at least one entry are
|
|
73
|
+
* returned.
|
|
74
|
+
*/
|
|
75
|
+
export declare function getLoadedPluginAdminExtensions(scopeKey: string): Array<{
|
|
76
|
+
pluginName: string;
|
|
77
|
+
state: AdminExtensionsState;
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Blocks registered at runtime via `api.registerBlock` by the plugins loaded
|
|
81
|
+
* in a scope — consumed by GET /api/v1/blocks, which merges them with
|
|
82
|
+
* `manifest.blocks`. Only plugins with at least one block are returned.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getLoadedPluginBlocks(scopeKey: string): Array<{
|
|
85
|
+
pluginName: string;
|
|
86
|
+
blocks: Array<PluginBlockManifest & {
|
|
87
|
+
pluginName: string;
|
|
88
|
+
}>;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Patterns registered at runtime via `api.registerPattern` by the plugins
|
|
92
|
+
* loaded in a scope — consumed by GET /api/v1/patterns, which merges them
|
|
93
|
+
* with `manifest.patterns`. Only plugins with at least one pattern are
|
|
94
|
+
* returned.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getLoadedPluginPatterns(scopeKey: string): Array<{
|
|
97
|
+
pluginName: string;
|
|
98
|
+
patterns: Array<PluginPatternManifest & {
|
|
99
|
+
pluginName: string;
|
|
100
|
+
}>;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Assets enqueued at runtime via `api.assets` by the plugins loaded in a
|
|
104
|
+
* scope — consumed by the public renderer (theme:head pipeline, Item 8).
|
|
105
|
+
* Only plugins with at least one asset are returned.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getLoadedPluginAssets(scopeKey: string): Array<{
|
|
108
|
+
pluginName: string;
|
|
109
|
+
assets: EnqueuedAsset[];
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Health checks registered at runtime via `api.health` by the plugins loaded
|
|
113
|
+
* in a scope — aggregated by GET /api/v1/health (Item 14). Only plugins with
|
|
114
|
+
* at least one check are returned.
|
|
115
|
+
*/
|
|
116
|
+
export declare function getLoadedPluginHealthChecks(scopeKey: string): Array<{
|
|
117
|
+
pluginName: string;
|
|
118
|
+
checks: Array<{
|
|
119
|
+
name: string;
|
|
120
|
+
check: PluginHealthCheck;
|
|
121
|
+
}>;
|
|
122
|
+
}>;
|
|
123
|
+
/**
|
|
124
|
+
* Removes all hooks from the loaded scopes and clears the state.
|
|
125
|
+
* Useful in tests and shutdown. When `registry` is given, hooks are removed
|
|
126
|
+
* from it; otherwise from the registry where they were registered.
|
|
62
127
|
*/
|
|
63
128
|
export declare function resetLoadedPlugins(registry?: HookRegistry): void;
|
|
64
129
|
/** Descarrega todos os plugins de um escopo (tenant desativado / LRU). */
|
|
65
130
|
export declare function unloadScope(scopeKey: string): boolean;
|
|
66
|
-
/**
|
|
131
|
+
/** Unloads a tenant (key `tenant:<id>` or the id itself). */
|
|
67
132
|
export declare function unloadTenant(tenantId: string): boolean;
|
|
68
133
|
/**
|
|
69
134
|
* Sincroniza o registry de um escopo com os plugins habilitados no banco.
|
|
70
135
|
* Seguro para chamar no boot de qualquer processo e em re-syncs (ex.: logo
|
|
71
|
-
*
|
|
136
|
+
* after enabling/disabling through the API).
|
|
72
137
|
*/
|
|
73
138
|
export declare function bootstrapPlugins(options: BootstrapPluginsOptions): Promise<PluginBootstrapResult>;
|
|
74
139
|
//# sourceMappingURL=bootstrap.d.ts.map
|
package/dist/bootstrap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"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"}
|
package/dist/bootstrap.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
// @oktis-works/plugin-runtime -
|
|
1
|
+
// @oktis-works/plugin-runtime - Plugin runtime bootstrap
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `plugin_tenants`
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// PLUGIN.md promises: "`register(registry)` is called when the plugin is
|
|
4
|
+
// loaded". This module honors it: discovers enabled plugins in the database
|
|
5
|
+
// (**platform** scope = `tenant_id IS NULL`; **tenant** scope = enabled
|
|
6
|
+
// `plugin_tenants`), imports the entrypoint, calls `register(registry)` with
|
|
7
|
+
// a hook API attributed to the plugin (BUSI-032/033) and syncs the scope
|
|
8
|
+
// registry.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
// - **
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
10
|
+
// Rules:
|
|
11
|
+
// - **never throws**: a broken plugin, invalid manifest, semantic
|
|
12
|
+
// incompatibility or an unavailable database never break the boot
|
|
13
|
+
// (failure isolation);
|
|
14
|
+
// - **syncs with the database**: anything that left the enabled list has
|
|
15
|
+
// its hooks removed from the registry (unload);
|
|
16
|
+
// - **idempotent**: an already-loaded plugin is not reloaded (no duplicate
|
|
17
|
+
// hooks) — repeated calls load only what is new;
|
|
18
|
+
// - **per scope**: load state is indexed by scope (platform or a tenant),
|
|
19
|
+
// so enabling/disabling in one tenant does not affect another.
|
|
19
20
|
import { createPluginHooksApi } from '@oktis-works/plugin-sdk';
|
|
20
21
|
import { PLUGIN_PERMISSION_DEFAULT } from '@oktis-works/validation';
|
|
21
22
|
import { PluginLoader } from './loader.js';
|
|
@@ -23,8 +24,10 @@ import { PluginSandbox } from './sandbox.js';
|
|
|
23
24
|
import { getHookRegistry, getHookRegistryStore } from './hook-registry.js';
|
|
24
25
|
/** Estado de carga por escopo (`''` = platform, `tenant:<id>` = tenant). */
|
|
25
26
|
const loadedScopes = new Map();
|
|
27
|
+
/** Platform scope uses the same key as the core ('platform') — external lookups match. */
|
|
28
|
+
const PLATFORM_SCOPE = 'platform';
|
|
26
29
|
function scopeKeyOf(tenantId) {
|
|
27
|
-
return tenantId ? `tenant:${tenantId}` :
|
|
30
|
+
return tenantId ? `tenant:${tenantId}` : PLATFORM_SCOPE;
|
|
28
31
|
}
|
|
29
32
|
function getScope(scopeKey, registry) {
|
|
30
33
|
let scope = loadedScopes.get(scopeKey);
|
|
@@ -34,7 +37,7 @@ function getScope(scopeKey, registry) {
|
|
|
34
37
|
}
|
|
35
38
|
return scope;
|
|
36
39
|
}
|
|
37
|
-
/**
|
|
40
|
+
/** Scope with registered loads (for inspection/runtime state). */
|
|
38
41
|
export function listLoadedScopes() {
|
|
39
42
|
return [...loadedScopes.entries()].map(([scope, entry]) => ({
|
|
40
43
|
scope,
|
|
@@ -42,7 +45,7 @@ export function listLoadedScopes() {
|
|
|
42
45
|
plugins: [...entry.plugins.keys()],
|
|
43
46
|
}));
|
|
44
47
|
}
|
|
45
|
-
/**
|
|
48
|
+
/** Gets the routes registered by plugins in a scope. */
|
|
46
49
|
export function getLoadedPluginRoutes(scopeKey) {
|
|
47
50
|
const scope = loadedScopes.get(scopeKey);
|
|
48
51
|
if (!scope)
|
|
@@ -52,16 +55,134 @@ export function getLoadedPluginRoutes(scopeKey) {
|
|
|
52
55
|
if (entry.internal) {
|
|
53
56
|
const routes = entry.internal.getRoutes();
|
|
54
57
|
if (routes.length > 0) {
|
|
55
|
-
|
|
58
|
+
const rpm = entry.manifest?.rateLimit?.rpm;
|
|
59
|
+
result.push({ pluginName, routes, ...(typeof rpm === 'number' && rpm > 0 ? { rpm } : {}) });
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
return result;
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
66
|
+
* Handlers de jobs dos plugins em um escopo (dispatch pelo worker:
|
|
67
|
+
* `plugin:{tenant}:{plugin}:{jobName}` → handler).
|
|
68
|
+
*/
|
|
69
|
+
export function getLoadedPluginJobHandlers(scopeKey) {
|
|
70
|
+
const scope = loadedScopes.get(scopeKey);
|
|
71
|
+
if (!scope)
|
|
72
|
+
return [];
|
|
73
|
+
const result = [];
|
|
74
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
75
|
+
if (entry.internal) {
|
|
76
|
+
const handlers = entry.internal.getJobHandlers?.();
|
|
77
|
+
if (handlers && handlers.size > 0) {
|
|
78
|
+
result.push({ pluginName, handlers });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Runtime `api.admin` state of the plugins loaded in a scope (slots
|
|
86
|
+
* registered at runtime) — consumed by GET /api/v1/plugins/extensions, which
|
|
87
|
+
* merges it with `manifest.admin`. Only states with at least one entry are
|
|
88
|
+
* returned.
|
|
89
|
+
*/
|
|
90
|
+
export function getLoadedPluginAdminExtensions(scopeKey) {
|
|
91
|
+
const scope = loadedScopes.get(scopeKey);
|
|
92
|
+
if (!scope)
|
|
93
|
+
return [];
|
|
94
|
+
const result = [];
|
|
95
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
96
|
+
const state = entry.internal?.getAdminState?.();
|
|
97
|
+
if (!state)
|
|
98
|
+
continue;
|
|
99
|
+
const hasEntries = state.menuPages.length > 0 ||
|
|
100
|
+
state.submenuPages.length > 0 ||
|
|
101
|
+
state.dashboardWidgets.length > 0 ||
|
|
102
|
+
state.metaBoxes.length > 0 ||
|
|
103
|
+
state.notices.length > 0 ||
|
|
104
|
+
state.actionLinks.length > 0 ||
|
|
105
|
+
(state.editors?.length ?? 0) > 0 ||
|
|
106
|
+
Object.keys(state.columns).length > 0 ||
|
|
107
|
+
Object.keys(state.columnRenderers).length > 0;
|
|
108
|
+
if (hasEntries)
|
|
109
|
+
result.push({ pluginName, state });
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Blocks registered at runtime via `api.registerBlock` by the plugins loaded
|
|
115
|
+
* in a scope — consumed by GET /api/v1/blocks, which merges them with
|
|
116
|
+
* `manifest.blocks`. Only plugins with at least one block are returned.
|
|
117
|
+
*/
|
|
118
|
+
export function getLoadedPluginBlocks(scopeKey) {
|
|
119
|
+
const scope = loadedScopes.get(scopeKey);
|
|
120
|
+
if (!scope)
|
|
121
|
+
return [];
|
|
122
|
+
const result = [];
|
|
123
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
124
|
+
const blocks = entry.internal?.getBlocks?.();
|
|
125
|
+
if (blocks && blocks.length > 0)
|
|
126
|
+
result.push({ pluginName, blocks });
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Patterns registered at runtime via `api.registerPattern` by the plugins
|
|
132
|
+
* loaded in a scope — consumed by GET /api/v1/patterns, which merges them
|
|
133
|
+
* with `manifest.patterns`. Only plugins with at least one pattern are
|
|
134
|
+
* returned.
|
|
135
|
+
*/
|
|
136
|
+
export function getLoadedPluginPatterns(scopeKey) {
|
|
137
|
+
const scope = loadedScopes.get(scopeKey);
|
|
138
|
+
if (!scope)
|
|
139
|
+
return [];
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
142
|
+
const patterns = entry.internal?.getPatterns?.();
|
|
143
|
+
if (patterns && patterns.length > 0)
|
|
144
|
+
result.push({ pluginName, patterns });
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Assets enqueued at runtime via `api.assets` by the plugins loaded in a
|
|
150
|
+
* scope — consumed by the public renderer (theme:head pipeline, Item 8).
|
|
151
|
+
* Only plugins with at least one asset are returned.
|
|
152
|
+
*/
|
|
153
|
+
export function getLoadedPluginAssets(scopeKey) {
|
|
154
|
+
const scope = loadedScopes.get(scopeKey);
|
|
155
|
+
if (!scope)
|
|
156
|
+
return [];
|
|
157
|
+
const result = [];
|
|
158
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
159
|
+
const assets = entry.internal?.getEnqueuedAssets?.();
|
|
160
|
+
if (assets && assets.length > 0)
|
|
161
|
+
result.push({ pluginName, assets });
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Health checks registered at runtime via `api.health` by the plugins loaded
|
|
167
|
+
* in a scope — aggregated by GET /api/v1/health (Item 14). Only plugins with
|
|
168
|
+
* at least one check are returned.
|
|
169
|
+
*/
|
|
170
|
+
export function getLoadedPluginHealthChecks(scopeKey) {
|
|
171
|
+
const scope = loadedScopes.get(scopeKey);
|
|
172
|
+
if (!scope)
|
|
173
|
+
return [];
|
|
174
|
+
const result = [];
|
|
175
|
+
for (const [pluginName, entry] of scope.plugins) {
|
|
176
|
+
const checks = entry.internal?.getHealthChecks?.();
|
|
177
|
+
if (checks && checks.length > 0)
|
|
178
|
+
result.push({ pluginName, checks });
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Removes all hooks from the loaded scopes and clears the state.
|
|
184
|
+
* Useful in tests and shutdown. When `registry` is given, hooks are removed
|
|
185
|
+
* from it; otherwise from the registry where they were registered.
|
|
65
186
|
*/
|
|
66
187
|
export function resetLoadedPlugins(registry) {
|
|
67
188
|
for (const [scopeKey, scope] of loadedScopes) {
|
|
@@ -93,7 +214,7 @@ export function unloadScope(scopeKey) {
|
|
|
93
214
|
loadedScopes.delete(scopeKey);
|
|
94
215
|
return true;
|
|
95
216
|
}
|
|
96
|
-
/**
|
|
217
|
+
/** Unloads a tenant (key `tenant:<id>` or the id itself). */
|
|
97
218
|
export function unloadTenant(tenantId) {
|
|
98
219
|
return unloadScope(scopeKeyOf(tenantId));
|
|
99
220
|
}
|
|
@@ -112,9 +233,9 @@ function unloadPlugin(scope, name) {
|
|
|
112
233
|
}
|
|
113
234
|
/**
|
|
114
235
|
* Registry que grava cada registro num sink (para poder descarregar depois),
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
236
|
+
* delegates to the real registry with the SDK-attributed `sourceId` and
|
|
237
|
+
* **applies the sandbox timeout** to the registered callback (WP5 — real
|
|
238
|
+
* protection against a hanging hook; documented as limited to async work).
|
|
118
239
|
*/
|
|
119
240
|
function recordingRegistry(registry, sink, sandbox) {
|
|
120
241
|
const acceptedArgsOf = (callback, explicit) => explicit ?? callback.length;
|
|
@@ -153,7 +274,7 @@ function enabledQuery(tenantId) {
|
|
|
153
274
|
/**
|
|
154
275
|
* Sincroniza o registry de um escopo com os plugins habilitados no banco.
|
|
155
276
|
* Seguro para chamar no boot de qualquer processo e em re-syncs (ex.: logo
|
|
156
|
-
*
|
|
277
|
+
* after enabling/disabling through the API).
|
|
157
278
|
*/
|
|
158
279
|
export async function bootstrapPlugins(options) {
|
|
159
280
|
const scopeKey = scopeKeyOf(options.tenantId);
|
|
@@ -161,7 +282,7 @@ export async function bootstrapPlugins(options) {
|
|
|
161
282
|
(options.tenantId ? getHookRegistryStore().tenant(options.tenantId) : getHookRegistry());
|
|
162
283
|
const statuses = options.enabledStatuses ?? ['ACTIVATED', 'RUNNING'];
|
|
163
284
|
const result = { enabled: [], loaded: [], unloaded: [], failed: [] };
|
|
164
|
-
// 1)
|
|
285
|
+
// 1) Who is enabled in the database? Failure = unknown state → touch nothing.
|
|
165
286
|
let enabled = null;
|
|
166
287
|
if (options.sql) {
|
|
167
288
|
try {
|
|
@@ -178,7 +299,7 @@ export async function bootstrapPlugins(options) {
|
|
|
178
299
|
result.enabled = enabled ?? [];
|
|
179
300
|
if (enabled === null)
|
|
180
301
|
return result;
|
|
181
|
-
// 2)
|
|
302
|
+
// 2) Discovers in the directory (missing directory = no plugins, not an error)
|
|
182
303
|
const loader = new PluginLoader(options.pluginsDir);
|
|
183
304
|
let discovered = [];
|
|
184
305
|
try {
|
|
@@ -194,16 +315,16 @@ export async function bootstrapPlugins(options) {
|
|
|
194
315
|
result.unloaded.push(name);
|
|
195
316
|
}
|
|
196
317
|
}
|
|
197
|
-
// 4)
|
|
318
|
+
// 4) Loads enabled-but-not-yet-loaded plugins, honoring dependency order
|
|
198
319
|
const enabledDiscovered = discovered.filter((entry) => enabled.includes(entry.manifest.name));
|
|
199
320
|
let levels;
|
|
200
321
|
try {
|
|
201
322
|
levels = loader.resolveDependencies(enabledDiscovered);
|
|
202
323
|
}
|
|
203
324
|
catch {
|
|
204
|
-
levels = enabledDiscovered.map((entry) => [entry]); //
|
|
325
|
+
levels = enabledDiscovered.map((entry) => [entry]); // ordering failed: arbitrary order
|
|
205
326
|
}
|
|
206
|
-
//
|
|
327
|
+
// Permission policy: configuration allowlist (or the full catalog).
|
|
207
328
|
const policy = options.permissionPolicy ?? PLUGIN_PERMISSION_DEFAULT;
|
|
208
329
|
const timeoutMs = options.hookTimeoutMs ?? 5000;
|
|
209
330
|
for (const level of levels) {
|
|
@@ -213,21 +334,31 @@ export async function bootstrapPlugins(options) {
|
|
|
213
334
|
continue;
|
|
214
335
|
try {
|
|
215
336
|
const loaded = await loader.loadPlugin(entry.manifest, entry.path);
|
|
216
|
-
//
|
|
337
|
+
// The sandbox validates the manifest against the POLICY (never against itself)
|
|
217
338
|
const sandbox = new PluginSandbox({ allowedPermissions: [...policy], timeoutMs });
|
|
218
339
|
sandbox.loadPlugin(entry.manifest, loaded.api);
|
|
219
340
|
// Contrato do PLUGIN.md: module.exports.register(registry)
|
|
220
341
|
const register = loaded.api.register;
|
|
221
342
|
if (typeof register !== 'function') {
|
|
222
|
-
throw new Error('
|
|
343
|
+
throw new Error('does not export register(registry)');
|
|
344
|
+
}
|
|
345
|
+
// Item 20: plugin schema migrations (manifest.migrations[]) run
|
|
346
|
+
// BEFORE register(), once per plugin (global registry in
|
|
347
|
+
// plugin_schema_migrations). Checksum drift fails the plugin isolated.
|
|
348
|
+
if (options.sql && Array.isArray(entry.manifest.migrations) && entry.manifest.migrations.length > 0) {
|
|
349
|
+
const { applyPluginMigrations } = await import('./plugin-migrations.js');
|
|
350
|
+
const migrationResult = await applyPluginMigrations(options.sql, name, entry.path, entry.manifest.migrations);
|
|
351
|
+
if (migrationResult.drift.length > 0) {
|
|
352
|
+
throw new Error(`Plugin migration drift: ${migrationResult.drift.join('; ')}`);
|
|
353
|
+
}
|
|
223
354
|
}
|
|
224
355
|
const registrations = [];
|
|
225
356
|
// Se o core forneceu apiDeps, cria a API completa (hooks + settings + query + routes + ...)
|
|
226
|
-
//
|
|
357
|
+
// Otherwise, only the legacy Hooks API is used (compatibility).
|
|
227
358
|
let apiToPass;
|
|
228
359
|
let internal;
|
|
229
360
|
if (options.apiDeps) {
|
|
230
|
-
//
|
|
361
|
+
// Merges the core deps with the plugin/runtime-specific ones
|
|
231
362
|
const deps = {
|
|
232
363
|
hooks: recordingRegistry(registry, registrations, sandbox),
|
|
233
364
|
manifest: entry.manifest,
|
|
@@ -242,20 +373,78 @@ export async function bootstrapPlugins(options) {
|
|
|
242
373
|
registerRoutes: options.apiDeps.registerRoutes ?? (() => { }),
|
|
243
374
|
registerContentFields: options.apiDeps.registerContentFields ?? (() => { }),
|
|
244
375
|
registerFieldTypes: options.apiDeps.registerFieldTypes ?? (() => { }),
|
|
376
|
+
registerPostTypes: options.apiDeps.registerPostTypes ?? (() => { }),
|
|
377
|
+
registerTaxonomies: options.apiDeps.registerTaxonomies ?? (() => { }),
|
|
378
|
+
// Core cache backend; absent → in-memory fallback in the factory
|
|
379
|
+
cache: options.apiDeps.cache,
|
|
380
|
+
// Backend de jobs do core; ausente → schedule/enqueue retornam false
|
|
381
|
+
jobs: options.apiDeps.jobs,
|
|
245
382
|
allowedPermissions: [...policy],
|
|
246
383
|
};
|
|
247
|
-
//
|
|
384
|
+
// Creates the full API using the internal factory
|
|
248
385
|
const { createPluginApi } = await import('./plugin-api.js');
|
|
249
386
|
const apiResult = createPluginApi(deps);
|
|
250
387
|
apiToPass = apiResult.api;
|
|
251
388
|
internal = apiResult.internal;
|
|
389
|
+
// Item 9: loads the manifest `locales[]` files before register()
|
|
390
|
+
// — translations already available during boot. File failure is
|
|
391
|
+
// silent (fallback = the key itself).
|
|
392
|
+
try {
|
|
393
|
+
const { loadPluginLocales } = await import('./i18n.js');
|
|
394
|
+
const i18n = apiResult.api.i18n;
|
|
395
|
+
if (i18n)
|
|
396
|
+
await loadPluginLocales(entry.path, entry.manifest, i18n);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// i18n unavailable — boot continues
|
|
400
|
+
}
|
|
252
401
|
}
|
|
253
402
|
else {
|
|
254
403
|
apiToPass = createPluginHooksApi(name, recordingRegistry(registry, registrations, sandbox));
|
|
255
404
|
}
|
|
256
|
-
register(
|
|
405
|
+
// register() may be async — we await it (with the same hook timeout)
|
|
406
|
+
// so rejections become an isolated failure instead of an unhandled
|
|
407
|
+
// rejection: previously a throwing async register escaped the
|
|
408
|
+
// try/catch and became an unhandled process error.
|
|
409
|
+
await Promise.race([
|
|
410
|
+
Promise.resolve(register(apiToPass)),
|
|
411
|
+
new Promise((_, reject) => {
|
|
412
|
+
const timer = setTimeout(() => reject(new Error(`register() excedeu ${timeoutMs}ms`)), timeoutMs);
|
|
413
|
+
timer.unref?.();
|
|
414
|
+
}),
|
|
415
|
+
]);
|
|
257
416
|
if (internal) {
|
|
258
|
-
scope.plugins.set(name, { registrations, internal });
|
|
417
|
+
scope.plugins.set(name, { registrations, internal, manifest: entry.manifest });
|
|
418
|
+
// Declarative manifest registration (v2): postTypes/taxonomies go
|
|
419
|
+
// through the same API validation (permission, slug, conflict) and
|
|
420
|
+
// the factory persists via the core registerPostTypes/registerTaxonomies.
|
|
421
|
+
if (entry.manifest.postTypes?.length || entry.manifest.taxonomies?.length) {
|
|
422
|
+
try {
|
|
423
|
+
const typedApi = apiToPass;
|
|
424
|
+
for (const postType of entry.manifest.postTypes ?? []) {
|
|
425
|
+
typedApi.addPostType(postType);
|
|
426
|
+
}
|
|
427
|
+
for (const taxonomy of entry.manifest.taxonomies ?? []) {
|
|
428
|
+
typedApi.addTaxonomy(taxonomy);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
console.warn(`[plugins] Failed to register declared post types/taxonomies for ${name}:`, e);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Jobs declarados no manifesto (v2): agenda recorrente via api.jobs
|
|
436
|
+
// (same validation/permission/ceiling as the imperative path).
|
|
437
|
+
if (entry.manifest.scheduleJobs?.length) {
|
|
438
|
+
const typedApi = apiToPass;
|
|
439
|
+
for (const job of entry.manifest.scheduleJobs) {
|
|
440
|
+
try {
|
|
441
|
+
await typedApi.jobs.schedule(job.name, { every: job.every, cron: job.cron });
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
console.warn(`[plugins] Failed to schedule declared job "${job.name}" for ${name}:`, e);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
259
448
|
// Persist field types registered by this plugin
|
|
260
449
|
if (internal.getFieldTypes && internal.getFieldTypes().length > 0) {
|
|
261
450
|
const fieldTypes = internal.getFieldTypes();
|
|
@@ -280,12 +469,12 @@ export async function bootstrapPlugins(options) {
|
|
|
280
469
|
}
|
|
281
470
|
}
|
|
282
471
|
else {
|
|
283
|
-
scope.plugins.set(name, { registrations });
|
|
472
|
+
scope.plugins.set(name, { registrations, manifest: entry.manifest });
|
|
284
473
|
}
|
|
285
474
|
result.loaded.push(name);
|
|
286
475
|
}
|
|
287
476
|
catch (error) {
|
|
288
|
-
//
|
|
477
|
+
// Failure isolation: a broken plugin never blocks the others or the boot
|
|
289
478
|
result.failed.push({ name, error: error instanceof Error ? error.message : String(error) });
|
|
290
479
|
}
|
|
291
480
|
}
|