@oktis-works/core 0.5.15 → 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 (55) hide show
  1. package/dist/cache/tags.d.ts +12 -0
  2. package/dist/cache/tags.d.ts.map +1 -0
  3. package/dist/cache/tags.js +55 -0
  4. package/dist/cache/tags.js.map +1 -0
  5. package/dist/comments/service.d.ts +54 -0
  6. package/dist/comments/service.d.ts.map +1 -0
  7. package/dist/comments/service.js +166 -0
  8. package/dist/comments/service.js.map +1 -0
  9. package/dist/content/service.d.ts.map +1 -1
  10. package/dist/content/service.js +24 -14
  11. package/dist/content/service.js.map +1 -1
  12. package/dist/fields/location-rules.d.ts +3 -1
  13. package/dist/fields/location-rules.d.ts.map +1 -1
  14. package/dist/fields/location-rules.js +1 -0
  15. package/dist/fields/location-rules.js.map +1 -1
  16. package/dist/fields/service.d.ts +41 -0
  17. package/dist/fields/service.d.ts.map +1 -1
  18. package/dist/fields/service.js +134 -0
  19. package/dist/fields/service.js.map +1 -1
  20. package/dist/fields/types/registry.d.ts +18 -0
  21. package/dist/fields/types/registry.d.ts.map +1 -1
  22. package/dist/fields/types/registry.js +13 -0
  23. package/dist/fields/types/registry.js.map +1 -1
  24. package/dist/hooks/points.d.ts +6 -0
  25. package/dist/hooks/points.d.ts.map +1 -1
  26. package/dist/hooks/points.js +7 -0
  27. package/dist/hooks/points.js.map +1 -1
  28. package/dist/index.d.ts +6 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +6 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/mail/service.d.ts +26 -0
  33. package/dist/mail/service.d.ts.map +1 -0
  34. package/dist/mail/service.js +117 -0
  35. package/dist/mail/service.js.map +1 -0
  36. package/dist/plugin/load.d.ts +80 -13
  37. package/dist/plugin/load.d.ts.map +1 -1
  38. package/dist/plugin/load.js +315 -50
  39. package/dist/plugin/load.js.map +1 -1
  40. package/dist/plugin/service.d.ts.map +1 -1
  41. package/dist/plugin/service.js +5 -1
  42. package/dist/plugin/service.js.map +1 -1
  43. package/dist/queue/producer.d.ts +12 -1
  44. package/dist/queue/producer.d.ts.map +1 -1
  45. package/dist/queue/producer.js +92 -0
  46. package/dist/queue/producer.js.map +1 -1
  47. package/dist/role/plugin-roles.d.ts +39 -0
  48. package/dist/role/plugin-roles.d.ts.map +1 -0
  49. package/dist/role/plugin-roles.js +161 -0
  50. package/dist/role/plugin-roles.js.map +1 -0
  51. package/dist/webhooks/service.d.ts +33 -0
  52. package/dist/webhooks/service.d.ts.map +1 -0
  53. package/dist/webhooks/service.js +57 -0
  54. package/dist/webhooks/service.js.map +1 -0
  55. package/package.json +6 -6
@@ -1,5 +1,70 @@
1
- import { type PluginBootstrapResult } from '@oktis-works/plugin-runtime';
2
- import type { RouteDefinition } from '@oktis-works/plugin-runtime';
1
+ import type { RouteDefinition, PluginBootstrapResult } from '@oktis-works/plugin-runtime';
2
+ import type { AdminExtensionsState, PluginBlockManifest, PluginPatternManifest } from '@oktis-works/types';
3
+ import type { EnqueuedAsset, PluginHealthCheck } from '@oktis-works/plugin-sdk';
4
+ /** Plugin job handler (dispatched by the worker). */
5
+ export type PluginJobHandlerFn = (payload: unknown, context: {
6
+ tenantId: string | null;
7
+ plugin: string;
8
+ jobName: string;
9
+ }) => void | Promise<void>;
10
+ /**
11
+ * Resolves the handler registered by a plugin for a job (via api.jobs.on).
12
+ * Scope: platform (`tenantId` null) or tenant (loaded on demand).
13
+ * Never throws — null when the plugin is disabled or the handler is absent.
14
+ */
15
+ export declare function getPluginJobHandler(tenantId: string | null, pluginName: string, jobName: string): Promise<PluginJobHandlerFn | null>;
16
+ /**
17
+ * `api.admin` slots loaded in a scope (platform `null` or `tenant:<id>`) —
18
+ * feeds GET /api/v1/plugins/extensions together with `manifest.admin`.
19
+ * Never throws: runtime unavailability becomes an empty list.
20
+ */
21
+ export declare function getPluginAdminExtensions(tenantId: string | null): Promise<Array<{
22
+ pluginName: string;
23
+ state: AdminExtensionsState;
24
+ }>>;
25
+ /**
26
+ * Blocks registered at runtime by the loaded plugins of a scope — feeds
27
+ * GET /api/v1/blocks together with `manifest.blocks`. Never throws: runtime
28
+ * unavailability becomes an empty list.
29
+ */
30
+ export declare function getPluginBlocks(tenantId: string | null): Promise<Array<{
31
+ pluginName: string;
32
+ blocks: Array<PluginBlockManifest & {
33
+ pluginName: string;
34
+ }>;
35
+ }>>;
36
+ /**
37
+ * Patterns registered at runtime by the loaded plugins of a scope — feeds
38
+ * GET /api/v1/patterns together with `manifest.patterns`. Never throws:
39
+ * runtime unavailability becomes an empty list.
40
+ */
41
+ export declare function getPluginPatterns(tenantId: string | null): Promise<Array<{
42
+ pluginName: string;
43
+ patterns: Array<PluginPatternManifest & {
44
+ pluginName: string;
45
+ }>;
46
+ }>>;
47
+ /**
48
+ * Assets enqueued by the loaded plugins of a scope — feeds the public
49
+ * renderer's theme:head pipeline (Item 8). Never throws: runtime
50
+ * unavailability becomes an empty list.
51
+ */
52
+ export declare function getPluginAssets(tenantId: string | null): Promise<Array<{
53
+ pluginName: string;
54
+ assets: EnqueuedAsset[];
55
+ }>>;
56
+ /**
57
+ * Health checks registered by the loaded plugins of a scope — aggregated by
58
+ * GET /api/v1/health (Item 14). Never throws: runtime unavailability becomes
59
+ * an empty list.
60
+ */
61
+ export declare function getPluginHealthChecks(tenantId: string | null): Promise<Array<{
62
+ pluginName: string;
63
+ checks: Array<{
64
+ name: string;
65
+ check: PluginHealthCheck;
66
+ }>;
67
+ }>>;
3
68
  export interface PluginsScopeState {
4
69
  /** `platform` ou `tenant:<id>`. */
5
70
  scope: string;
@@ -21,36 +86,38 @@ export interface PluginsRuntimeState {
21
86
  export interface PluginRouteEntry {
22
87
  pluginName: string;
23
88
  routes: RouteDefinition[];
89
+ /** rpm do rate limit por plugin (manifest rateLimit.rpm, Item 17). */
90
+ rpm?: number;
24
91
  }
25
92
  /** Registra rotas de um plugin (chamado durante bootstrap). */
26
- export declare function registerPluginRoutes(pluginName: string, routes: RouteDefinition[]): void;
93
+ export declare function registerPluginRoutes(pluginName: string, routes: RouteDefinition[], rpm?: number): void;
27
94
  /** Remove rotas de um plugin (quando desativado/desinstalado). */
28
95
  export declare function unregisterPluginRoutes(pluginName: string): boolean;
29
- /** Obtém todas as rotas registradas para montagem no router da API. */
96
+ /** Gets every route registered for mounting on the API router. */
30
97
  export declare function getRegisteredPluginRoutes(): PluginRouteEntry[];
31
98
  /**
32
99
  * Sincroniza o registry **platform** com os plugins habilitados no banco.
33
- * Force implícito: é a chamada explícita de boot/ativação — reflete o banco
34
- * na hora. Idempotente e nunca lança.
100
+ * Implicit force: this is the explicit boot/activation call — reflects the
101
+ * database immediately. Idempotent and never throws.
35
102
  */
36
103
  export declare function syncEnabledPlugins(options?: {
37
104
  force?: boolean;
38
105
  }): Promise<PluginBootstrapResult>;
39
106
  /**
40
- * Garante que os plugins do tenant estão carregados no registry dele.
107
+ * Ensures the tenant's plugins are loaded into its own registry.
41
108
  * Chamado sob demanda (middleware/API, web, worker) — com cache de TTL para
42
- * não bater no banco a cada requisição. Nunca lança.
109
+ * avoids hitting the database on every request. Never throws.
43
110
  */
44
111
  export declare function ensureTenantPluginsLoaded(tenantId: string, options?: {
45
112
  force?: boolean;
46
113
  }): Promise<PluginBootstrapResult>;
47
114
  /**
48
- * Invalida o cache/registry de um tenant (após ativar/desativar vínculo,
49
- * desinstalar ou mudar permissões). A próxima requisição do tenant recarrega.
115
+ * Invalidates a tenant's cache/registry (after enabling/disabling a link,
116
+ * uninstalling or changing permissions). The tenant's next request reloads.
50
117
  */
51
- export declare function invalidateTenantPlugins(tenantId: string): void;
52
- /** Invalida o escopo platform (após mudanças globais). */
118
+ export declare function invalidateTenantPlugins(tenantId: string): Promise<void>;
119
+ /** Invalidates the platform scope (after global changes). */
53
120
  export declare function invalidatePlatformPlugins(): void;
54
- /** Estado de carga por escopo — superfície de observabilidade (WP2). */
121
+ /** Per-scope load state — observability surface (WP2). */
55
122
  export declare function getPluginsRuntimeState(): PluginsRuntimeState;
56
123
  //# sourceMappingURL=load.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/plugin/load.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,qBAAqB,EAE3B,MAAM,6BAA6B,CAAC;AAWrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAID,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,CAGxF;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,uEAAuE;AACvE,wBAAgB,yBAAyB,IAAI,gBAAgB,EAAE,CAE9D;AAkOD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAS1G;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,OAAO,CAAC,qBAAqB,CAAC,CAUhC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAO9D;AAED,0DAA0D;AAC1D,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,IAAI,mBAAmB,CAa5D"}
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/plugin/load.ts"],"names":[],"mappings":"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"}
@@ -1,20 +1,20 @@
1
- // @oktis-works/core - Sincronização de plugins habilitados em runtime
1
+ // @oktis-works/core - Enabled plugins runtime sync
2
2
  //
3
- // O PLUGIN.md promete que `register(registry)` é chamado quando o plugin é
4
- // carregado — aqui o core cumpre isso para todos os processos (API, web,
5
- // worker, dev). Dois escopos:
3
+ // PLUGIN.md promises that `register(registry)` is called when the plugin is
4
+ // loaded — the core honors it for every process (API, web, worker, dev). Two
5
+ // scopes:
6
6
  //
7
- // - **platform** (`syncEnabledPlugins`): plugins `tenant_id IS NULL`,
8
- // habilitados globalmente → registry platform, carregado no boot;
9
- // - **tenant** (`ensureTenantPluginsLoaded`): plugins vinculados em
10
- // `plugin_tenants` (status ENABLED) → registry do tenant, carregado sob
11
- // demanda na primeira requisição daquele tenant (cache com TTL + LRU).
7
+ // - **platform** (`syncEnabledPlugins`): plugins with `tenant_id IS NULL`,
8
+ // enabled globally → platform registry, loaded at boot;
9
+ // - **tenant** (`ensureTenantPluginsLoaded`): plugins linked in
10
+ // `plugin_tenants` (ENABLED status) → tenant registry, loaded on demand
11
+ // at that tenant's first request (TTL + LRU cache).
12
12
  //
13
- // Nunca lança: falha de banco, diretório ausente ou plugin quebrado são
14
- // reportadas no resultado (e persistidas em `plugins.error`), sem derrubar o
15
- // boot. Falhas ficam visíveis em `getPluginsRuntimeState()` (rota
16
- // `/api/v1/plugins/runtime` + badge no admin) — não são silenciadas.
17
- import { bootstrapPlugins, getHookRegistryStore, unloadTenant, getLoadedPluginRoutes, } from '@oktis-works/plugin-runtime';
13
+ // Never throws: database failure, missing directory or a broken plugin are
14
+ // reported in the result (and persisted in `plugins.error`) without breaking
15
+ // the boot. Failures are visible in `getPluginsRuntimeState()` (the
16
+ // `/api/v1/plugins/runtime` route + admin badge) — never silenced.
17
+ import { createHash } from 'node:crypto';
18
18
  import { getConnection } from '@oktis-works/database';
19
19
  import { loadConfig } from '@oktis-works/config';
20
20
  import { resolvePluginPermissionPolicy } from '@oktis-works/validation';
@@ -24,23 +24,143 @@ import { getEventBus } from '../events/bus.js';
24
24
  import { tenantService } from '../tenant/service.js';
25
25
  import { userService } from '../user/service.js';
26
26
  import { fieldGroupService } from '../fields/service.js';
27
+ import { postTypeRegistry } from '../content/post-types.js';
28
+ import { taxonomyService } from '../taxonomies/service.js';
29
+ import { pluginRolesService } from '../role/plugin-roles.js';
30
+ import { mailService } from '../mail/service.js';
31
+ import { auditService } from '../observability/audit.js';
32
+ import { getCache } from '../cache/index.js';
33
+ import { queueProducer } from '../queue/producer.js';
27
34
  import { getCurrentTenantIdSync, getCurrentUserIdSync, getCurrentRequestIdSync } from '../tenant/context.js';
35
+ // Dynamic imports for the circular dependency with plugin-runtime
36
+ // (non-literal require so TypeScript cannot resolve it at build time)
37
+ const getPluginRuntime = async () => {
38
+ return require('@oktis-works/plugin-runtime');
39
+ };
40
+ let _pluginRuntimeCache = null;
41
+ const getPluginRuntimeCached = async () => {
42
+ if (!_pluginRuntimeCache) {
43
+ _pluginRuntimeCache = await getPluginRuntime();
44
+ }
45
+ return _pluginRuntimeCache;
46
+ };
47
+ const getBootstrapPlugins = async () => (await getPluginRuntimeCached()).bootstrapPlugins;
48
+ const getHookRegistryStore = async () => (await getPluginRuntimeCached()).getHookRegistryStore;
49
+ const unloadTenant = async () => (await getPluginRuntimeCached()).unloadTenant;
50
+ const getLoadedPluginRoutes = async () => (await getPluginRuntimeCached()).getLoadedPluginRoutes;
51
+ const getLoadedPluginJobHandlers = async () => (await getPluginRuntimeCached()).getLoadedPluginJobHandlers;
52
+ const getLoadedPluginAdminExtensions = async () => (await getPluginRuntimeCached()).getLoadedPluginAdminExtensions;
53
+ const getLoadedPluginBlocks = async () => (await getPluginRuntimeCached()).getLoadedPluginBlocks;
54
+ const getLoadedPluginPatterns = async () => (await getPluginRuntimeCached()).getLoadedPluginPatterns;
55
+ const getLoadedPluginAssets = async () => (await getPluginRuntimeCached()).getLoadedPluginAssets;
56
+ const getLoadedPluginHealthChecks = async () => (await getPluginRuntimeCached()).getLoadedPluginHealthChecks;
57
+ /** Deterministic plugin job id (schedules are idempotent by it). */
58
+ function pluginJobId(tenantId, pluginName, jobName) {
59
+ return `plugin:${tenantId ?? 'platform'}:${pluginName}:${jobName}`;
60
+ }
61
+ /**
62
+ * Resolves the handler registered by a plugin for a job (via api.jobs.on).
63
+ * Scope: platform (`tenantId` null) or tenant (loaded on demand).
64
+ * Never throws — null when the plugin is disabled or the handler is absent.
65
+ */
66
+ export async function getPluginJobHandler(tenantId, pluginName, jobName) {
67
+ try {
68
+ const entries = await (await getLoadedPluginJobHandlers())(scopeKey(tenantId));
69
+ for (const entry of entries) {
70
+ if (entry.pluginName === pluginName) {
71
+ return entry.handlers.get(jobName) ?? null;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * `api.admin` slots loaded in a scope (platform `null` or `tenant:<id>`) —
82
+ * feeds GET /api/v1/plugins/extensions together with `manifest.admin`.
83
+ * Never throws: runtime unavailability becomes an empty list.
84
+ */
85
+ export async function getPluginAdminExtensions(tenantId) {
86
+ try {
87
+ return await (await getLoadedPluginAdminExtensions())(scopeKey(tenantId));
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ }
93
+ /**
94
+ * Blocks registered at runtime by the loaded plugins of a scope — feeds
95
+ * GET /api/v1/blocks together with `manifest.blocks`. Never throws: runtime
96
+ * unavailability becomes an empty list.
97
+ */
98
+ export async function getPluginBlocks(tenantId) {
99
+ try {
100
+ return await (await getLoadedPluginBlocks())(scopeKey(tenantId));
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ /**
107
+ * Patterns registered at runtime by the loaded plugins of a scope — feeds
108
+ * GET /api/v1/patterns together with `manifest.patterns`. Never throws:
109
+ * runtime unavailability becomes an empty list.
110
+ */
111
+ export async function getPluginPatterns(tenantId) {
112
+ try {
113
+ return await (await getLoadedPluginPatterns())(scopeKey(tenantId));
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
119
+ /**
120
+ * Assets enqueued by the loaded plugins of a scope — feeds the public
121
+ * renderer's theme:head pipeline (Item 8). Never throws: runtime
122
+ * unavailability becomes an empty list.
123
+ */
124
+ export async function getPluginAssets(tenantId) {
125
+ try {
126
+ return await (await getLoadedPluginAssets())(scopeKey(tenantId));
127
+ }
128
+ catch {
129
+ return [];
130
+ }
131
+ }
132
+ /**
133
+ * Health checks registered by the loaded plugins of a scope — aggregated by
134
+ * GET /api/v1/health (Item 14). Never throws: runtime unavailability becomes
135
+ * an empty list.
136
+ */
137
+ export async function getPluginHealthChecks(tenantId) {
138
+ try {
139
+ return await (await getLoadedPluginHealthChecks())(scopeKey(tenantId));
140
+ }
141
+ catch {
142
+ return [];
143
+ }
144
+ }
28
145
  const pluginRoutesRegistry = new Map();
29
146
  /** Registra rotas de um plugin (chamado durante bootstrap). */
30
- export function registerPluginRoutes(pluginName, routes) {
147
+ export function registerPluginRoutes(pluginName, routes, rpm) {
31
148
  if (routes.length === 0)
32
149
  return;
33
- pluginRoutesRegistry.set(pluginName, { pluginName, routes });
150
+ pluginRoutesRegistry.set(pluginName, { pluginName, routes, ...(rpm ? { rpm } : {}) });
34
151
  }
35
152
  /** Remove rotas de um plugin (quando desativado/desinstalado). */
36
153
  export function unregisterPluginRoutes(pluginName) {
37
154
  return pluginRoutesRegistry.delete(pluginName);
38
155
  }
39
- /** Obtém todas as rotas registradas para montagem no router da API. */
156
+ /** Gets every route registered for mounting on the API router. */
40
157
  export function getRegisteredPluginRoutes() {
41
158
  return [...pluginRoutesRegistry.values()];
42
159
  }
43
- /** Estados por escopo — ordem de iteração = LRU. */
160
+ /** Item 21: registro das consultas preparadas por hash (teto LRU). */
161
+ const PREPARED_CACHE_MAX = 200;
162
+ const preparedPluginStatements = new Map();
163
+ /** Per-scope states — iteration order = LRU. */
44
164
  const states = new Map();
45
165
  const inflight = new Map();
46
166
  const PLATFORM_KEY = 'platform';
@@ -63,9 +183,31 @@ function createApiDeps(tenantId) {
63
183
  db: {
64
184
  query: async (sql, params) => {
65
185
  const conn = getConnection();
186
+ // Item 21: registro de prepared statements por hash — dedupe + LRU +
187
+ // contagem de uso por consulta (identidade da consulta preparada em
188
+ // api.query; o driver reutiliza o plano para consultas repetidas).
189
+ const hash = createHash('sha256').update(sql).digest('hex').slice(0, 24);
190
+ let entry = preparedPluginStatements.get(hash);
191
+ if (!entry) {
192
+ entry = { sql, uses: 0 };
193
+ preparedPluginStatements.set(hash, entry);
194
+ if (preparedPluginStatements.size > PREPARED_CACHE_MAX) {
195
+ const oldest = preparedPluginStatements.keys().next().value;
196
+ if (oldest)
197
+ preparedPluginStatements.delete(oldest);
198
+ }
199
+ }
200
+ entry.uses += 1;
66
201
  return conn.unsafe(sql, params ?? []);
67
202
  },
68
203
  },
204
+ // api.cache backend (Item 2) — lazy so contexts that never use the
205
+ // plugin cache don't need it at boot.
206
+ cache: {
207
+ get: (key) => getCache().get(key),
208
+ set: (key, value, ttlSeconds) => getCache().set(key, value, ttlSeconds),
209
+ del: (key) => getCache().del(key),
210
+ },
69
211
  getTenantId: () => tenantId ?? getCurrentTenantIdSync(),
70
212
  getUserId: () => getCurrentUserIdSync(),
71
213
  getRequestId: () => getCurrentRequestIdSync(),
@@ -78,14 +220,14 @@ function createApiDeps(tenantId) {
78
220
  return user ? { id: user.id, email: user.email, name: user.name, roles: user.roles } : null;
79
221
  },
80
222
  registerRoutes: (routes) => {
81
- // As rotas registradas aqui são coletadas no bootstrap pelo próprio
82
- // PluginAPI (getLoadedPluginRoutes) — neste ponto só registramos o log.
83
- console.debug(`[PluginAPI] ${routes.length} rota(s) registrada(s) — será coletada no bootstrap`);
223
+ // Routes registered here are collected at bootstrap by the PluginAPI
224
+ // itself (getLoadedPluginRoutes) — this point only logs.
225
+ console.debug(`[PluginAPI] ${routes.length} route(s) registered — collected at bootstrap`);
84
226
  },
85
227
  registerContentFields: async (fields) => {
86
- // Guard explícito: com noUncheckedIndexedAccess, fields[0] é T | undefined
87
- // mesmo com o length check acima — o desctructuring com verificação
88
- // deixa o narrowing para o TS e mantém a mesma semântica.
228
+ // Explicit guard: with noUncheckedIndexedAccess, fields[0] is
229
+ // T | undefined even with the length check above — checked
230
+ // destructuring leaves the narrowing to TS with the same semantics.
89
231
  const firstField = fields[0];
90
232
  if (!firstField)
91
233
  return;
@@ -116,12 +258,135 @@ function createApiDeps(tenantId) {
116
258
  fields: fieldInputs,
117
259
  });
118
260
  }
119
- console.debug(`[PluginAPI] ${fields.length} campo(s) de conteúdo registrado(s) no field group "${groupKey}"`);
261
+ console.debug(`[PluginAPI] ${fields.length} content field(s) registered in field group "${groupKey}"`);
120
262
  }
121
263
  catch (error) {
122
- console.error('[PluginAPI] Falha ao registrar campos de conteúdo:', error);
264
+ console.error('[PluginAPI] Failed to register content fields:', error);
123
265
  }
124
266
  },
267
+ registerFieldTypes: async (fieldTypes) => {
268
+ try {
269
+ // Same package — relative import (a self-import of '@oktis-works/core'
270
+ // does not resolve on a fresh checkout, where dist/ does not exist yet).
271
+ const { fieldTypeExtensionService } = await import('../fields/service.js');
272
+ for (const ft of fieldTypes) {
273
+ await fieldTypeExtensionService.register(ft);
274
+ }
275
+ await fieldTypeExtensionService.syncToRegistry();
276
+ console.debug(`[PluginAPI] ${fieldTypes.length} tipo(s) de campo registrado(s) no registry`);
277
+ }
278
+ catch (error) {
279
+ console.error('[PluginAPI] Falha ao registrar tipos de campo:', error);
280
+ }
281
+ },
282
+ registerPostTypes: async (postTypes) => {
283
+ // Idempotent persistence for plugin Custom Post Types (item 1 of the
284
+ // plugin-parity-plan): existe com mesmo sourceId → atualiza; existe com
285
+ // outra origem (CORE/ADMIN/outro plugin) → warn + skip (nunca rouba).
286
+ try {
287
+ for (const type of postTypes) {
288
+ const supports = ['title', 'editor'];
289
+ if (type.revisions)
290
+ supports.push('revisions');
291
+ const existing = await postTypeRegistry.getBySlug(type.key);
292
+ if (existing) {
293
+ if (existing.source === 'PLUGIN' && existing.sourceId === type.pluginName) {
294
+ await postTypeRegistry.update(type.key, {
295
+ singularLabel: type.label,
296
+ pluralLabel: type.plural ?? existing.pluralLabel,
297
+ menuIcon: type.icon ?? existing.menuIcon,
298
+ supports,
299
+ });
300
+ }
301
+ else {
302
+ console.warn(`[PluginAPI] Post type "${type.key}" already exists (${existing.source}) — "${type.pluginName}" cannot claim it`);
303
+ }
304
+ continue;
305
+ }
306
+ await postTypeRegistry.create({
307
+ name: type.key,
308
+ slug: type.key,
309
+ pluralLabel: type.plural ?? `${type.label}s`,
310
+ singularLabel: type.label,
311
+ source: 'PLUGIN',
312
+ sourceId: type.pluginName,
313
+ supports,
314
+ hasArchive: true,
315
+ menuIcon: type.icon ?? 'box',
316
+ singleton: false,
317
+ public: true,
318
+ });
319
+ }
320
+ console.debug(`[PluginAPI] ${postTypes.length} post type(s) de plugin persistido(s)`);
321
+ }
322
+ catch (error) {
323
+ console.error('[PluginAPI] Falha ao registrar post types:', error);
324
+ }
325
+ },
326
+ registerTaxonomies: async (taxonomies) => {
327
+ // Idempotent persistence for plugin Custom Taxonomies — ownership
328
+ // stored in meta.plugin (schema has no sourceId for taxonomies).
329
+ try {
330
+ for (const taxonomy of taxonomies) {
331
+ const existing = await taxonomyService.getBySlug(taxonomy.key);
332
+ if (existing) {
333
+ const owner = existing.meta?.['plugin'];
334
+ if (owner !== undefined && owner !== taxonomy.pluginName) {
335
+ console.warn(`[PluginAPI] Taxonomy "${taxonomy.key}" belongs to plugin "${String(owner)}" — "${taxonomy.pluginName}" cannot claim it`);
336
+ continue;
337
+ }
338
+ await taxonomyService.update(taxonomy.key, {
339
+ name: taxonomy.label,
340
+ hierarchical: taxonomy.hierarchical ?? false,
341
+ attachTo: taxonomy.postTypes,
342
+ meta: { ...(existing.meta ?? {}), plugin: taxonomy.pluginName },
343
+ });
344
+ continue;
345
+ }
346
+ await taxonomyService.create({
347
+ name: taxonomy.label,
348
+ slug: taxonomy.key,
349
+ hierarchical: taxonomy.hierarchical ?? false,
350
+ attachTo: taxonomy.postTypes,
351
+ meta: { plugin: taxonomy.pluginName },
352
+ source: 'PLUGIN',
353
+ });
354
+ }
355
+ console.debug(`[PluginAPI] ${taxonomies.length} taxonomia(s) de plugin persistida(s)`);
356
+ }
357
+ catch (error) {
358
+ console.error('[PluginAPI] Falha ao registrar taxonomias:', error);
359
+ }
360
+ },
361
+ jobs: {
362
+ schedule: async (jobName, spec, ctx) => {
363
+ // Recurrence on the `plugins` queue — deterministic idempotent jobId
364
+ return queueProducer.scheduleRepeatable('plugin.job', pluginJobId(ctx.tenantId, ctx.pluginName, jobName), { plugin: ctx.pluginName, job: jobName, scheduled: true }, spec, { tenantId: ctx.tenantId ?? 'default' });
365
+ },
366
+ cancel: async (jobName, ctx) => {
367
+ return queueProducer.cancelRepeatable('plugin.job', pluginJobId(ctx.tenantId, ctx.pluginName, jobName));
368
+ },
369
+ enqueue: async (jobName, payload, ctx) => {
370
+ return queueProducer.enqueue('plugin.job', { plugin: ctx.pluginName, job: jobName, data: payload }, { tenantId: ctx.tenantId ?? 'default' });
371
+ },
372
+ },
373
+ roles: {
374
+ add: (input) => pluginRolesService.add(input),
375
+ remove: (input) => pluginRolesService.remove(input),
376
+ addPermission: (input) => pluginRolesService.addPermission(input),
377
+ },
378
+ mail: {
379
+ send: (input) => mailService.enqueue(input),
380
+ },
381
+ audit: {
382
+ record: (entry) => auditService.record({
383
+ tenantId: entry.tenantId ?? undefined,
384
+ action: entry.action,
385
+ resourceType: entry.resourceType,
386
+ resourceId: entry.resourceId,
387
+ changes: entry.changes,
388
+ }),
389
+ },
125
390
  };
126
391
  }
127
392
  function runtimeOptions(tenantId) {
@@ -143,7 +408,7 @@ function runtimeOptions(tenantId) {
143
408
  * Persiste o resultado da carga em `plugins.error`/`status`:
144
409
  * - falha nova → `status = LOAD_FAILED` + mensagem;
145
410
  * - sucesso depois de falha → `ACTIVATED` + `error = NULL`.
146
- * Só grava quando o estado mudou (nada de UPDATE por request). Nunca lança.
411
+ * Only writes when the state changed (no UPDATE per request). Never throws.
147
412
  */
148
413
  async function persistScopeResult(state) {
149
414
  const sql = getConnection();
@@ -165,7 +430,7 @@ async function persistScopeResult(state) {
165
430
  }
166
431
  }
167
432
  catch {
168
- // banco indisponível não pode derrubar a carga — visibilidade fica no estado em memória
433
+ // an unavailable database cannot break the load — visibility stays in the in-memory state
169
434
  }
170
435
  }
171
436
  async function syncScope(tenantId, force) {
@@ -184,12 +449,12 @@ async function syncScope(tenantId, force) {
184
449
  return running;
185
450
  const promise = (async () => {
186
451
  try {
187
- const result = await bootstrapPlugins(runtimeOptions(tenantId));
452
+ const result = await (await getBootstrapPlugins())(runtimeOptions(tenantId));
188
453
  // Coleta rotas registradas pelos plugins neste escopo
189
454
  const scopeKey = key; // 'platform' ou 'tenant:<id>'
190
- const pluginRoutes = getLoadedPluginRoutes(scopeKey);
191
- for (const { pluginName, routes } of pluginRoutes) {
192
- registerPluginRoutes(pluginName, routes);
455
+ const pluginRoutes = await (await getLoadedPluginRoutes())(scopeKey);
456
+ for (const { pluginName, routes, rpm } of pluginRoutes) {
457
+ registerPluginRoutes(pluginName, routes, rpm);
193
458
  }
194
459
  // Remove rotas de plugins que foram descarregados (unloaded)
195
460
  for (const pluginName of result.unloaded) {
@@ -206,11 +471,11 @@ async function syncScope(tenantId, force) {
206
471
  states.set(key, state);
207
472
  await persistScopeResult(state);
208
473
  state.errors = new Map(result.failed.map((failure) => [failure.name, failure.error]));
209
- evictOldTenants(config.plugins.maxLoadedTenants);
474
+ await evictOldTenants(config.plugins.maxLoadedTenants);
210
475
  return result;
211
476
  }
212
477
  catch (error) {
213
- console.warn(`[plugins] sync indisponível (${key}): ${error instanceof Error ? error.message : String(error)}`);
478
+ console.warn(`[plugins] sync unavailable (${key}): ${error instanceof Error ? error.message : String(error)}`);
214
479
  const fallback = { enabled: [], loaded: [], unloaded: [], failed: [] };
215
480
  if (!states.has(key)) {
216
481
  states.set(key, { key, tenantId, loadedAt: 0, result: fallback, errors: new Map() });
@@ -225,7 +490,7 @@ async function syncScope(tenantId, force) {
225
490
  return promise;
226
491
  }
227
492
  /** Evita crescer sem limite: descarta tenants com carga mais antiga. */
228
- function evictOldTenants(maxTenants) {
493
+ async function evictOldTenants(maxTenants) {
229
494
  if (states.size <= maxTenants)
230
495
  return;
231
496
  for (const [key, state] of [...states.entries()]) {
@@ -234,28 +499,28 @@ function evictOldTenants(maxTenants) {
234
499
  if (state.tenantId === null)
235
500
  continue; // platform nunca sai
236
501
  states.delete(key);
237
- unloadTenant(state.tenantId);
238
- getHookRegistryStore().invalidateTenant(state.tenantId);
502
+ await (await unloadTenant())(state.tenantId);
503
+ (await (await getHookRegistryStore())).invalidateTenant(state.tenantId);
239
504
  }
240
505
  }
241
506
  /**
242
507
  * Sincroniza o registry **platform** com os plugins habilitados no banco.
243
- * Force implícito: é a chamada explícita de boot/ativação — reflete o banco
244
- * na hora. Idempotente e nunca lança.
508
+ * Implicit force: this is the explicit boot/activation call — reflects the
509
+ * database immediately. Idempotent and never throws.
245
510
  */
246
511
  export async function syncEnabledPlugins(options = {}) {
247
512
  try {
248
513
  return await syncScope(null, options.force ?? true);
249
514
  }
250
515
  catch (error) {
251
- console.warn(`[plugins] sync indisponível: ${error instanceof Error ? error.message : String(error)}`);
516
+ console.warn(`[plugins] sync unavailable: ${error instanceof Error ? error.message : String(error)}`);
252
517
  return { enabled: [], loaded: [], unloaded: [], failed: [] };
253
518
  }
254
519
  }
255
520
  /**
256
- * Garante que os plugins do tenant estão carregados no registry dele.
521
+ * Ensures the tenant's plugins are loaded into its own registry.
257
522
  * Chamado sob demanda (middleware/API, web, worker) — com cache de TTL para
258
- * não bater no banco a cada requisição. Nunca lança.
523
+ * avoids hitting the database on every request. Never throws.
259
524
  */
260
525
  export async function ensureTenantPluginsLoaded(tenantId, options = {}) {
261
526
  if (!tenantId)
@@ -264,29 +529,29 @@ export async function ensureTenantPluginsLoaded(tenantId, options = {}) {
264
529
  return await syncScope(tenantId, options.force ?? false);
265
530
  }
266
531
  catch (error) {
267
- console.warn(`[plugins] sync do tenant indisponível: ${error instanceof Error ? error.message : String(error)}`);
532
+ console.warn(`[plugins] tenant sync unavailable: ${error instanceof Error ? error.message : String(error)}`);
268
533
  return { enabled: [], loaded: [], unloaded: [], failed: [] };
269
534
  }
270
535
  }
271
536
  /**
272
- * Invalida o cache/registry de um tenant (após ativar/desativar vínculo,
273
- * desinstalar ou mudar permissões). A próxima requisição do tenant recarrega.
537
+ * Invalidates a tenant's cache/registry (after enabling/disabling a link,
538
+ * uninstalling or changing permissions). The tenant's next request reloads.
274
539
  */
275
- export function invalidateTenantPlugins(tenantId) {
540
+ export async function invalidateTenantPlugins(tenantId) {
276
541
  if (!tenantId)
277
542
  return;
278
543
  const key = scopeKey(tenantId);
279
544
  states.delete(key);
280
545
  inflight.delete(key);
281
- unloadTenant(tenantId);
282
- getHookRegistryStore().invalidateTenant(tenantId);
546
+ await (await unloadTenant())(tenantId);
547
+ (await (await getHookRegistryStore())).invalidateTenant(tenantId);
283
548
  }
284
- /** Invalida o escopo platform (após mudanças globais). */
549
+ /** Invalidates the platform scope (after global changes). */
285
550
  export function invalidatePlatformPlugins() {
286
551
  states.delete(PLATFORM_KEY);
287
552
  inflight.delete(PLATFORM_KEY);
288
553
  }
289
- /** Estado de carga por escopo — superfície de observabilidade (WP2). */
554
+ /** Per-scope load state — observability surface (WP2). */
290
555
  export function getPluginsRuntimeState() {
291
556
  return {
292
557
  generatedAt: new Date().toISOString(),