@oktis-works/plugin-runtime 0.5.10 → 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.
Files changed (46) hide show
  1. package/dist/bootstrap.d.ts +86 -21
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +254 -42
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/content-query.d.ts +29 -0
  6. package/dist/content-query.d.ts.map +1 -0
  7. package/dist/content-query.js +109 -0
  8. package/dist/content-query.js.map +1 -0
  9. package/dist/hook-registry.d.ts +21 -21
  10. package/dist/hook-registry.d.ts.map +1 -1
  11. package/dist/hook-registry.js +26 -26
  12. package/dist/hook-registry.js.map +1 -1
  13. package/dist/http.d.ts +49 -0
  14. package/dist/http.d.ts.map +1 -0
  15. package/dist/http.js +313 -0
  16. package/dist/http.js.map +1 -0
  17. package/dist/i18n.d.ts +16 -0
  18. package/dist/i18n.d.ts.map +1 -0
  19. package/dist/i18n.js +73 -0
  20. package/dist/i18n.js.map +1 -0
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/loader.js +3 -3
  26. package/dist/loader.js.map +1 -1
  27. package/dist/manifest.d.ts +11 -6
  28. package/dist/manifest.d.ts.map +1 -1
  29. package/dist/manifest.js +227 -17
  30. package/dist/manifest.js.map +1 -1
  31. package/dist/plugin-api.d.ts +191 -38
  32. package/dist/plugin-api.d.ts.map +1 -1
  33. package/dist/plugin-api.js +572 -10
  34. package/dist/plugin-api.js.map +1 -1
  35. package/dist/plugin-migrations.d.ts +15 -0
  36. package/dist/plugin-migrations.d.ts.map +1 -0
  37. package/dist/plugin-migrations.js +68 -0
  38. package/dist/plugin-migrations.js.map +1 -0
  39. package/dist/sandbox.d.ts +13 -11
  40. package/dist/sandbox.d.ts.map +1 -1
  41. package/dist/sandbox.js +11 -9
  42. package/dist/sandbox.js.map +1 -1
  43. package/dist/style-isolation.d.ts +3 -3
  44. package/dist/style-isolation.js +6 -6
  45. package/dist/style-isolation.js.map +1 -1
  46. package/package.json +7 -6
@@ -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
- /** Diretório de plugins (o chamador resolve — plugin-runtime não conhece o core). */
8
+ /** Plugins directory (the caller resolves it — plugin-runtime doesn't know the core). */
8
9
  pluginsDir: string;
9
- /** Cliente postgres para ler os habilitados; omitido = estado desconhecido (no-op). */
10
+ /** Postgres client to read enabled plugins; omitted = unknown state (no-op). */
10
11
  sql?: BootstrapPluginsSql;
11
- /** Registry alvo (default: registry do escopo — platform ou `tenantId`). */
12
+ /** Target registry (default: the scope registry — platform or `tenantId`). */
12
13
  registry?: HookRegistry;
13
- /** Statuses considerados habilitados (default: ACTIVATED, RUNNING). */
14
+ /** Statuses considered enabled (default: ACTIVATED, RUNNING). */
14
15
  enabledStatuses?: string[];
15
16
  /**
16
- * Escopo de carga: `null`/omitido → platform (`plugins.tenant_id IS NULL`);
17
- * string → tenant (`plugin_tenants` com status ENABLED).
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
- * Política de permissões concedidas (allowlist). Default: catálogo completo
22
- * de permissões do CMS. Nunca é preenchida com as permissões do manifesto.
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
- /** Timeout de callbacks de hook em ms (0 = sem timeout). Default 5000. */
26
+ /** Hook callback timeout in ms (0 = no timeout). Default 5000. */
26
27
  hookTimeoutMs?: number;
27
28
  /**
28
- * Dependências reais para a PluginAPI (settings, eventBus, db, etc.).
29
- * Se omitido, usa apenas o Hooks API legado (compatibilidade).
30
- * O core injeta isto com serviços reais.
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
- /** Registrados nesta sincronização. */
38
+ /** Loaded in this sync. */
38
39
  loaded: string[];
39
- /** Hooks removidos nesta sincronização (saíram dos habilitados). */
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
- /** Escopo com carga registrada (para inspeção/estado de runtime). */
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
- /** Obtém as rotas registradas pelos plugins em um escopo. */
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
- * Remove todos os hooks dos escopos carregados e limpa o estado.
60
- * Útil em testes e em shutdown. Quando `registry` é informado, os hooks são
61
- * removidos dele; caso contrário, do registry em que foram registrados.
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
- /** Descarrega um tenant (chave `tenant:<id>` ou o próprio id). */
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
- * após ativar/desativar pela API).
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
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,KAAK,EAAE,qBAAqB,EAAqB,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEjG,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,uBAAuB;IACtC,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,GAAG,CAAC,EAAE,mBAAmB,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,yDAAyD;IACzD,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAoCD,qEAAqE;AACrE,wBAAgB,gBAAgB,IAAI,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAMtG;AAED,6DAA6D;AAC7D,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAAC,CAahH;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAWhE;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWrD;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEtD;AA6DD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4HvG"}
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 - Bootstrap de plugins em runtime
1
+ // @oktis-works/plugin-runtime - Plugin runtime bootstrap
2
2
  //
3
- // O PLUGIN.md promete: "`register(registry)` é chamado quando o plugin é
4
- // carregado". Este módulo cumpre a promessa: descobre os plugins habilitados
5
- // no banco (escopo **platform** = `tenant_id IS NULL`; escopo **tenant** =
6
- // `plugin_tenants` habilitados), importa o entrypoint, chama
7
- // `register(registry)` com uma API de hooks atribuída ao plugin (BUSI-032/033)
8
- // e sincroniza o registry do escopo.
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
- // Regras:
11
- // - **nunca lança**: plugin quebrado, manifesto inválido, incompatibilidade
12
- // semântica ou banco indisponível não derrubam o boot (isolamento de falha);
13
- // - **sincroniza com o banco**: quem saiu da lista de habilitados tem os
14
- // hooks removidos do registry (unload);
15
- // - **idempotente**: plugin já carregado não é recarregado (sem hooks
16
- // duplicados) — chamadas repetidas carregam só o que é novo;
17
- // - **por escopo**: o estado de carga é indexado por escopo (platform ou um
18
- // tenant), então ativar/desativar num tenant não afeta outro.
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
- /** Escopo com carga registrada (para inspeção/estado de runtime). */
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
- /** Obtém as rotas registradas pelos plugins em um escopo. */
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
- result.push({ pluginName, routes });
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
- * Remove todos os hooks dos escopos carregados e limpa o estado.
63
- * Útil em testes e em shutdown. Quando `registry` é informado, os hooks são
64
- * removidos dele; caso contrário, do registry em que foram registrados.
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
- /** Descarrega um tenant (chave `tenant:<id>` ou o próprio id). */
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
- * delega ao registry real com `sourceId` atribuído pelo SDK e **aplica o
116
- * timeout do sandbox** no callback registrado (WP5 — proteção real contra hook
117
- * pendurado; documentada como limitada a trabalho assíncrono).
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
- * após ativar/desativar pela API).
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) Quem está habilitado no banco? Falha = estado desconhecido → não toca em nada.
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) Descobre no diretório (diretório ausente = nenhum plugin, não é erro)
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) Carrega habilitados ainda não carregados, respeitando ordem de dependência
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]); // ordenação falhou: ordem arbitrária
325
+ levels = enabledDiscovered.map((entry) => [entry]); // ordering failed: arbitrary order
205
326
  }
206
- // Política de permissões: allowlist de configuração (ou catálogo completo).
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
- // O sandbox valida o manifesto contra a POLÍTICA (nunca contra si mesmo)
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('não exporta register(registry)');
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
- // Caso contrário, usa apenas o Hooks API legado (compatibilidade).
357
+ // Otherwise, only the legacy Hooks API is used (compatibility).
227
358
  let apiToPass;
228
359
  let internal;
229
360
  if (options.apiDeps) {
230
- // Mescla os deps do core com os específicos do plugin/runtime
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,
@@ -241,28 +372,109 @@ export async function bootstrapPlugins(options) {
241
372
  getUserById: options.apiDeps.getUserById ?? (async () => null),
242
373
  registerRoutes: options.apiDeps.registerRoutes ?? (() => { }),
243
374
  registerContentFields: options.apiDeps.registerContentFields ?? (() => { }),
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,
244
382
  allowedPermissions: [...policy],
245
383
  };
246
- // Cria a API completa usando a fábrica interna
384
+ // Creates the full API using the internal factory
247
385
  const { createPluginApi } = await import('./plugin-api.js');
248
386
  const apiResult = createPluginApi(deps);
249
387
  apiToPass = apiResult.api;
250
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
+ }
251
401
  }
252
402
  else {
253
403
  apiToPass = createPluginHooksApi(name, recordingRegistry(registry, registrations, sandbox));
254
404
  }
255
- register(apiToPass);
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
+ ]);
256
416
  if (internal) {
257
- 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
+ }
448
+ // Persist field types registered by this plugin
449
+ if (internal.getFieldTypes && internal.getFieldTypes().length > 0) {
450
+ const fieldTypes = internal.getFieldTypes();
451
+ try {
452
+ // Import fieldTypeExtensionService dynamically to avoid circular deps:
453
+ // core depends on plugin-runtime, so plugin-runtime cannot declare core
454
+ // as a dependency (and packages/core/dist does not exist yet when this
455
+ // package is built). A non-literal specifier keeps TypeScript from
456
+ // resolving the module at compile time; resolution happens at runtime.
457
+ const coreModuleId = '@oktis-works/core';
458
+ const core = (await import(coreModuleId));
459
+ const { fieldTypeExtensionService } = core;
460
+ for (const ft of fieldTypes) {
461
+ await fieldTypeExtensionService.register(ft);
462
+ }
463
+ // Sync to in-memory registry
464
+ await fieldTypeExtensionService.syncToRegistry();
465
+ }
466
+ catch (e) {
467
+ console.warn(`[plugins] Failed to persist field types for ${name}:`, e);
468
+ }
469
+ }
258
470
  }
259
471
  else {
260
- scope.plugins.set(name, { registrations });
472
+ scope.plugins.set(name, { registrations, manifest: entry.manifest });
261
473
  }
262
474
  result.loaded.push(name);
263
475
  }
264
476
  catch (error) {
265
- // Isolamento de falha: plugin quebrado não impede os demais nem o boot
477
+ // Failure isolation: a broken plugin never blocks the others or the boot
266
478
  result.failed.push({ name, error: error instanceof Error ? error.message : String(error) });
267
479
  }
268
480
  }