@open-mercato/shared 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7195.1.b5efd1d51a
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/ai/opencode-tool-parts.js +48 -0
- package/dist/lib/ai/opencode-tool-parts.js.map +7 -0
- package/dist/lib/ai/token-count.js +11 -0
- package/dist/lib/ai/token-count.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +14 -1
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +9 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/registry.js +9 -0
- package/dist/lib/commands/registry.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/di/container.js +7 -10
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +22 -2
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/openapi/generator.js +3 -2
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/openapi/index.js +3 -2
- package/dist/lib/openapi/index.js.map +2 -2
- package/dist/lib/seed/crypto.js +73 -0
- package/dist/lib/seed/crypto.js.map +7 -0
- package/dist/lib/seed/index.js +4 -0
- package/dist/lib/seed/index.js.map +7 -0
- package/dist/lib/seed/loader.js +73 -0
- package/dist/lib/seed/loader.js.map +7 -0
- package/dist/lib/seed/types.js +33 -0
- package/dist/lib/seed/types.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +28 -9
- package/dist/modules/events/factory.js.map +2 -2
- package/package.json +3 -2
- package/src/lib/ai/__tests__/opencode-tool-parts.test.ts +81 -0
- package/src/lib/ai/__tests__/token-count.test.ts +20 -0
- package/src/lib/ai/opencode-tool-parts.ts +80 -0
- package/src/lib/ai/token-count.ts +21 -0
- package/src/lib/bootstrap/dynamicLoader.ts +24 -1
- package/src/lib/commands/__tests__/command-bus.test.ts +64 -0
- package/src/lib/commands/__tests__/registry.test.ts +35 -0
- package/src/lib/commands/command-bus.ts +16 -1
- package/src/lib/commands/registry.ts +11 -0
- package/src/lib/commands/types.ts +31 -0
- package/src/lib/di/__tests__/bootstrap-cache.test.ts +59 -3
- package/src/lib/di/container.ts +19 -15
- package/src/lib/encryption/__tests__/subscriber.kms-recovery.test.ts +112 -0
- package/src/lib/encryption/subscriber.ts +28 -1
- package/src/lib/openapi/__tests__/generator-response-fallback.test.ts +73 -0
- package/src/lib/openapi/generator.ts +3 -3
- package/src/lib/openapi/index.ts +1 -1
- package/src/lib/seed/__tests__/seed-crypto.test.ts +64 -0
- package/src/lib/seed/crypto.ts +87 -0
- package/src/lib/seed/index.ts +3 -0
- package/src/lib/seed/loader.ts +124 -0
- package/src/lib/seed/types.ts +48 -0
- package/src/modules/events/__tests__/factory.test.ts +96 -0
- package/src/modules/events/factory.ts +41 -10
- package/src/modules/events/types.ts +44 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
|
-
import { buildOpenApiDocument, generateMarkdownFromOpenApi } from "./generator.js";
|
|
2
|
+
import { buildOpenApiDocument, generateMarkdownFromOpenApi, zodToJsonSchema } from "./generator.js";
|
|
3
3
|
export * from "./crud.js";
|
|
4
4
|
import { sanitizeOpenApiDocument } from "./sanitize.js";
|
|
5
5
|
import { attachOpenApiDocsToModules } from "./attach-docs.js";
|
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
attachOpenApiDocsToModules,
|
|
8
8
|
buildOpenApiDocument,
|
|
9
9
|
generateMarkdownFromOpenApi,
|
|
10
|
-
sanitizeOpenApiDocument
|
|
10
|
+
sanitizeOpenApiDocument,
|
|
11
|
+
zodToJsonSchema
|
|
11
12
|
};
|
|
12
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/openapi/index.ts"],
|
|
4
|
-
"sourcesContent": ["export * from './types'\nexport { buildOpenApiDocument, generateMarkdownFromOpenApi } from './generator'\nexport * from './crud'\nexport { sanitizeOpenApiDocument } from './sanitize'\nexport { attachOpenApiDocsToModules } from './attach-docs'\n"],
|
|
5
|
-
"mappings": "AAAA,cAAc;AACd,SAAS,sBAAsB,
|
|
4
|
+
"sourcesContent": ["export * from './types'\nexport { buildOpenApiDocument, generateMarkdownFromOpenApi, zodToJsonSchema, type JsonSchema } from './generator'\nexport * from './crud'\nexport { sanitizeOpenApiDocument } from './sanitize'\nexport { attachOpenApiDocsToModules } from './attach-docs'\n"],
|
|
5
|
+
"mappings": "AAAA,cAAc;AACd,SAAS,sBAAsB,6BAA6B,uBAAwC;AACpG,cAAc;AACd,SAAS,+BAA+B;AACxC,SAAS,kCAAkC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { decryptWithAesGcm, encryptWithAesGcm } from "../encryption/aes.js";
|
|
3
|
+
import {
|
|
4
|
+
ENCRYPTED_SEED_ALGORITHM,
|
|
5
|
+
ENCRYPTED_SEED_FORMAT,
|
|
6
|
+
ENCRYPTED_SEED_VERSION,
|
|
7
|
+
encryptedSeedEnvelopeSchema,
|
|
8
|
+
seedDocumentSchema
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
const SEED_KEY_ENV = "OM_SEED_KEY";
|
|
11
|
+
const SEED_KEY_BYTES = 32;
|
|
12
|
+
function normalizeKey(value) {
|
|
13
|
+
return value.trim().replace(/(?:^['"]|['"]$)/g, "");
|
|
14
|
+
}
|
|
15
|
+
function generateSeedKey() {
|
|
16
|
+
return crypto.randomBytes(SEED_KEY_BYTES).toString("base64");
|
|
17
|
+
}
|
|
18
|
+
function resolveSeedKey(explicit) {
|
|
19
|
+
const raw = normalizeKey(explicit ?? process.env[SEED_KEY_ENV] ?? "");
|
|
20
|
+
if (!raw) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`[internal] Seed key missing: set ${SEED_KEY_ENV} (base64, 32 bytes) or pass --key. Generate one with "mercato seeds keygen".`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
let decoded;
|
|
26
|
+
try {
|
|
27
|
+
decoded = Buffer.from(raw, "base64");
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`[internal] ${SEED_KEY_ENV} must be base64-encoded.`);
|
|
30
|
+
}
|
|
31
|
+
if (decoded.length !== SEED_KEY_BYTES) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`[internal] ${SEED_KEY_ENV} must decode to ${SEED_KEY_BYTES} bytes (got ${decoded.length}). Generate one with "mercato seeds keygen".`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return raw;
|
|
37
|
+
}
|
|
38
|
+
function encryptSeedDocument(document, key) {
|
|
39
|
+
const doc = seedDocumentSchema.parse(document);
|
|
40
|
+
const json = JSON.stringify(doc);
|
|
41
|
+
const { value } = encryptWithAesGcm(json, key);
|
|
42
|
+
if (!value) {
|
|
43
|
+
throw new Error("[internal] Seed encryption produced no payload.");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
format: ENCRYPTED_SEED_FORMAT,
|
|
47
|
+
version: ENCRYPTED_SEED_VERSION,
|
|
48
|
+
algorithm: ENCRYPTED_SEED_ALGORITHM,
|
|
49
|
+
payload: value
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function decryptSeedEnvelope(envelope, key) {
|
|
53
|
+
const parsed = encryptedSeedEnvelopeSchema.parse(envelope);
|
|
54
|
+
const json = decryptWithAesGcm(parsed.payload, key);
|
|
55
|
+
if (json === null) {
|
|
56
|
+
throw new Error("[internal] Seed decryption failed \u2014 wrong key or corrupted payload.");
|
|
57
|
+
}
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = JSON.parse(json);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error("[internal] Decrypted seed is not valid JSON.");
|
|
63
|
+
}
|
|
64
|
+
return seedDocumentSchema.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
SEED_KEY_ENV,
|
|
68
|
+
decryptSeedEnvelope,
|
|
69
|
+
encryptSeedDocument,
|
|
70
|
+
generateSeedKey,
|
|
71
|
+
resolveSeedKey
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/seed/crypto.ts"],
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '../encryption/aes'\nimport {\n ENCRYPTED_SEED_ALGORITHM,\n ENCRYPTED_SEED_FORMAT,\n ENCRYPTED_SEED_VERSION,\n encryptedSeedEnvelopeSchema,\n seedDocumentSchema,\n type EncryptedSeedEnvelope,\n type SeedDocument,\n} from './types'\n\n/**\n * Distribution-layer key for encrypting seed blobs committed to the repo. This is\n * deliberately separate from the platform's per-tenant field-encryption keys\n * (Vault / TENANT_DATA_ENCRYPTION_*): rotating or sharing the seed key never\n * touches data-at-rest encryption.\n */\nexport const SEED_KEY_ENV = 'OM_SEED_KEY'\n\nconst SEED_KEY_BYTES = 32\n\nfunction normalizeKey(value: string): string {\n return value.trim().replace(/(?:^['\"]|['\"]$)/g, '')\n}\n\n/** Generate a fresh base64-encoded 32-byte seed key. */\nexport function generateSeedKey(): string {\n return crypto.randomBytes(SEED_KEY_BYTES).toString('base64')\n}\n\n/**\n * Resolve and validate the seed key from an explicit value or `OM_SEED_KEY`.\n * Throws a clear, actionable error when missing or malformed.\n */\nexport function resolveSeedKey(explicit?: string | null): string {\n const raw = normalizeKey(explicit ?? process.env[SEED_KEY_ENV] ?? '')\n if (!raw) {\n throw new Error(\n `[internal] Seed key missing: set ${SEED_KEY_ENV} (base64, 32 bytes) or pass --key. Generate one with \"mercato seeds keygen\".`,\n )\n }\n let decoded: Buffer\n try {\n decoded = Buffer.from(raw, 'base64')\n } catch {\n throw new Error(`[internal] ${SEED_KEY_ENV} must be base64-encoded.`)\n }\n if (decoded.length !== SEED_KEY_BYTES) {\n throw new Error(\n `[internal] ${SEED_KEY_ENV} must decode to ${SEED_KEY_BYTES} bytes (got ${decoded.length}). Generate one with \"mercato seeds keygen\".`,\n )\n }\n return raw\n}\n\n/** Encrypt a validated seed document into the opaque, committable envelope. */\nexport function encryptSeedDocument(document: SeedDocument, key: string): EncryptedSeedEnvelope {\n const doc = seedDocumentSchema.parse(document)\n const json = JSON.stringify(doc)\n const { value } = encryptWithAesGcm(json, key)\n if (!value) {\n throw new Error('[internal] Seed encryption produced no payload.')\n }\n return {\n format: ENCRYPTED_SEED_FORMAT,\n version: ENCRYPTED_SEED_VERSION,\n algorithm: ENCRYPTED_SEED_ALGORITHM,\n payload: value,\n }\n}\n\n/** Decrypt and validate an envelope back into a seed document. */\nexport function decryptSeedEnvelope(envelope: unknown, key: string): SeedDocument {\n const parsed = encryptedSeedEnvelopeSchema.parse(envelope)\n const json = decryptWithAesGcm(parsed.payload, key)\n if (json === null) {\n throw new Error('[internal] Seed decryption failed \u2014 wrong key or corrupted payload.')\n }\n let raw: unknown\n try {\n raw = JSON.parse(json)\n } catch {\n throw new Error('[internal] Decrypted seed is not valid JSON.')\n }\n return seedDocumentSchema.parse(raw)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,YAAY;AACnB,SAAS,mBAAmB,yBAAyB;AACrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAQA,MAAM,eAAe;AAE5B,MAAM,iBAAiB;AAEvB,SAAS,aAAa,OAAuB;AAC3C,SAAO,MAAM,KAAK,EAAE,QAAQ,oBAAoB,EAAE;AACpD;AAGO,SAAS,kBAA0B;AACxC,SAAO,OAAO,YAAY,cAAc,EAAE,SAAS,QAAQ;AAC7D;AAMO,SAAS,eAAe,UAAkC;AAC/D,QAAM,MAAM,aAAa,YAAY,QAAQ,IAAI,YAAY,KAAK,EAAE;AACpE,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR,oCAAoC,YAAY;AAAA,IAClD;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,cAAU,OAAO,KAAK,KAAK,QAAQ;AAAA,EACrC,QAAQ;AACN,UAAM,IAAI,MAAM,cAAc,YAAY,0BAA0B;AAAA,EACtE;AACA,MAAI,QAAQ,WAAW,gBAAgB;AACrC,UAAM,IAAI;AAAA,MACR,cAAc,YAAY,mBAAmB,cAAc,eAAe,QAAQ,MAAM;AAAA,IAC1F;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAAwB,KAAoC;AAC9F,QAAM,MAAM,mBAAmB,MAAM,QAAQ;AAC7C,QAAM,OAAO,KAAK,UAAU,GAAG;AAC/B,QAAM,EAAE,MAAM,IAAI,kBAAkB,MAAM,GAAG;AAC7C,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,EACX;AACF;AAGO,SAAS,oBAAoB,UAAmB,KAA2B;AAChF,QAAM,SAAS,4BAA4B,MAAM,QAAQ;AACzD,QAAM,OAAO,kBAAkB,OAAO,SAAS,GAAG;AAClD,MAAI,SAAS,MAAM;AACjB,UAAM,IAAI,MAAM,0EAAqE;AAAA,EACvF;AACA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,IAAI;AAAA,EACvB,QAAQ;AACN,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AACA,SAAO,mBAAmB,MAAM,GAAG;AACrC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { resolveEntityIdFromMetadata } from "../encryption/entityIds.js";
|
|
2
|
+
import { seedDocumentSchema } from "./types.js";
|
|
3
|
+
class SeedDryRunRollback extends Error {
|
|
4
|
+
}
|
|
5
|
+
function resolveEntityClass(meta) {
|
|
6
|
+
return meta.class ?? meta.className ?? meta.name;
|
|
7
|
+
}
|
|
8
|
+
function buildEntityIdIndex(em) {
|
|
9
|
+
const storage = em.getMetadata();
|
|
10
|
+
const all = (typeof storage.getAll === "function" ? storage.getAll() : storage.metadata) ?? {};
|
|
11
|
+
const list = all instanceof Map ? [...all.values()] : Array.isArray(all) ? all : Object.values(all);
|
|
12
|
+
const index = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const meta of list) {
|
|
14
|
+
if (!meta || meta.abstract) continue;
|
|
15
|
+
const entityId = resolveEntityIdFromMetadata(meta);
|
|
16
|
+
if (!entityId) continue;
|
|
17
|
+
if (!index.has(entityId)) index.set(entityId, meta);
|
|
18
|
+
}
|
|
19
|
+
return index;
|
|
20
|
+
}
|
|
21
|
+
function hasProperty(meta, name) {
|
|
22
|
+
return Boolean(meta.properties && meta.properties[name]);
|
|
23
|
+
}
|
|
24
|
+
async function loadSeedDocument(em, document, scope, options = {}) {
|
|
25
|
+
const doc = seedDocumentSchema.parse(document);
|
|
26
|
+
const index = buildEntityIdIndex(em);
|
|
27
|
+
const total = doc.records.length;
|
|
28
|
+
let created = 0;
|
|
29
|
+
let skipped = 0;
|
|
30
|
+
const run = async (tem) => {
|
|
31
|
+
for (let i = 0; i < doc.records.length; i += 1) {
|
|
32
|
+
const record = doc.records[i];
|
|
33
|
+
const meta = index.get(record.entity);
|
|
34
|
+
if (!meta) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[internal] Unknown seed entity "${record.entity}" at record ${i}: not a registered entity id.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const entityClass = resolveEntityClass(meta);
|
|
40
|
+
const data = { ...record.data };
|
|
41
|
+
if (hasProperty(meta, "tenantId")) data.tenantId = scope.tenantId;
|
|
42
|
+
if (hasProperty(meta, "organizationId")) data.organizationId = scope.organizationId;
|
|
43
|
+
if (record.match && record.match.length) {
|
|
44
|
+
const where = {};
|
|
45
|
+
for (const field of record.match) where[field] = data[field];
|
|
46
|
+
if (hasProperty(meta, "tenantId")) where.tenantId = scope.tenantId;
|
|
47
|
+
if (hasProperty(meta, "organizationId")) where.organizationId = scope.organizationId;
|
|
48
|
+
const existing = await tem.findOne(entityClass, where);
|
|
49
|
+
if (existing) {
|
|
50
|
+
skipped += 1;
|
|
51
|
+
options.onProgress?.({ index: i, total, entity: record.entity, action: "skipped" });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const entity = tem.create(entityClass, data);
|
|
56
|
+
tem.persist(entity);
|
|
57
|
+
await tem.flush();
|
|
58
|
+
created += 1;
|
|
59
|
+
options.onProgress?.({ index: i, total, entity: record.entity, action: "created" });
|
|
60
|
+
}
|
|
61
|
+
if (options.dryRun) throw new SeedDryRunRollback();
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
await em.transactional(run);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (!(err instanceof SeedDryRunRollback)) throw err;
|
|
67
|
+
}
|
|
68
|
+
return { total, created, skipped };
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
loadSeedDocument
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/seed/loader.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager, EntityMetadata } from '@mikro-orm/postgresql'\nimport { resolveEntityIdFromMetadata } from '../encryption/entityIds'\nimport { seedDocumentSchema, type SeedDocument } from './types'\n\nexport type SeedLoadScope = {\n tenantId: string\n organizationId: string\n}\n\nexport type SeedLoadProgress = {\n index: number\n total: number\n entity: string\n action: 'created' | 'skipped'\n}\n\nexport type SeedLoadOptions = {\n /** Apply inside a transaction and roll it back; reports what would happen. */\n dryRun?: boolean\n onProgress?: (progress: SeedLoadProgress) => void\n}\n\nexport type SeedLoadResult = {\n total: number\n created: number\n skipped: number\n}\n\nclass SeedDryRunRollback extends Error {}\n\nfunction resolveEntityClass(meta: EntityMetadata<any>): unknown {\n return (meta as any).class ?? meta.className ?? meta.name\n}\n\nfunction buildEntityIdIndex(em: EntityManager): Map<string, EntityMetadata<any>> {\n const storage = em.getMetadata() as unknown as {\n getAll?: () =>\n | Map<unknown, EntityMetadata<any>>\n | Record<string, EntityMetadata<any>>\n | EntityMetadata<any>[]\n metadata?: Record<string, EntityMetadata<any>>\n }\n const all = (typeof storage.getAll === 'function' ? storage.getAll() : storage.metadata) ?? {}\n // MikroORM v7's instance getAll() returns a Map; older shapes returned a plain\n // object or array. Normalize all three to a flat list.\n const list: EntityMetadata<any>[] =\n all instanceof Map ? [...all.values()] : Array.isArray(all) ? all : Object.values(all)\n const index = new Map<string, EntityMetadata<any>>()\n for (const meta of list) {\n if (!meta || (meta as any).abstract) continue\n const entityId = resolveEntityIdFromMetadata(meta)\n if (!entityId) continue\n if (!index.has(entityId)) index.set(entityId, meta)\n }\n return index\n}\n\nfunction hasProperty(meta: EntityMetadata<any>, name: string): boolean {\n return Boolean(meta.properties && (meta.properties as Record<string, unknown>)[name])\n}\n\n/**\n * Insert seed records through the ORM so the tenant-data-encryption subscriber\n * encrypts marked fields at rest automatically. Records are applied in order;\n * `tenantId`/`organizationId` are injected from `scope` for every entity that\n * declares them. Records with a `match` list are skipped when an existing row\n * matches (idempotent re-runs); match fields MUST be non-encrypted natural keys.\n */\nexport async function loadSeedDocument(\n em: EntityManager,\n document: SeedDocument,\n scope: SeedLoadScope,\n options: SeedLoadOptions = {},\n): Promise<SeedLoadResult> {\n const doc = seedDocumentSchema.parse(document)\n const index = buildEntityIdIndex(em)\n const total = doc.records.length\n let created = 0\n let skipped = 0\n\n const run = async (tem: EntityManager) => {\n for (let i = 0; i < doc.records.length; i += 1) {\n const record = doc.records[i]\n const meta = index.get(record.entity)\n if (!meta) {\n throw new Error(\n `[internal] Unknown seed entity \"${record.entity}\" at record ${i}: not a registered entity id.`,\n )\n }\n const entityClass = resolveEntityClass(meta)\n const data: Record<string, unknown> = { ...record.data }\n if (hasProperty(meta, 'tenantId')) data.tenantId = scope.tenantId\n if (hasProperty(meta, 'organizationId')) data.organizationId = scope.organizationId\n\n if (record.match && record.match.length) {\n const where: Record<string, unknown> = {}\n for (const field of record.match) where[field] = data[field]\n if (hasProperty(meta, 'tenantId')) where.tenantId = scope.tenantId\n if (hasProperty(meta, 'organizationId')) where.organizationId = scope.organizationId\n const existing = await tem.findOne(entityClass as any, where as any)\n if (existing) {\n skipped += 1\n options.onProgress?.({ index: i, total, entity: record.entity, action: 'skipped' })\n continue\n }\n }\n\n const entity = tem.create(entityClass as any, data as any)\n tem.persist(entity)\n await tem.flush()\n created += 1\n options.onProgress?.({ index: i, total, entity: record.entity, action: 'created' })\n }\n if (options.dryRun) throw new SeedDryRunRollback()\n }\n\n try {\n await em.transactional(run)\n } catch (err) {\n if (!(err instanceof SeedDryRunRollback)) throw err\n }\n\n return { total, created, skipped }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,mCAAmC;AAC5C,SAAS,0BAA6C;AA0BtD,MAAM,2BAA2B,MAAM;AAAC;AAExC,SAAS,mBAAmB,MAAoC;AAC9D,SAAQ,KAAa,SAAS,KAAK,aAAa,KAAK;AACvD;AAEA,SAAS,mBAAmB,IAAqD;AAC/E,QAAM,UAAU,GAAG,YAAY;AAO/B,QAAM,OAAO,OAAO,QAAQ,WAAW,aAAa,QAAQ,OAAO,IAAI,QAAQ,aAAa,CAAC;AAG7F,QAAM,OACJ,eAAe,MAAM,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,OAAO,GAAG;AACvF,QAAM,QAAQ,oBAAI,IAAiC;AACnD,aAAW,QAAQ,MAAM;AACvB,QAAI,CAAC,QAAS,KAAa,SAAU;AACrC,UAAM,WAAW,4BAA4B,IAAI;AACjD,QAAI,CAAC,SAAU;AACf,QAAI,CAAC,MAAM,IAAI,QAAQ,EAAG,OAAM,IAAI,UAAU,IAAI;AAAA,EACpD;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAA2B,MAAuB;AACrE,SAAO,QAAQ,KAAK,cAAe,KAAK,WAAuC,IAAI,CAAC;AACtF;AASA,eAAsB,iBACpB,IACA,UACA,OACA,UAA2B,CAAC,GACH;AACzB,QAAM,MAAM,mBAAmB,MAAM,QAAQ;AAC7C,QAAM,QAAQ,mBAAmB,EAAE;AACnC,QAAM,QAAQ,IAAI,QAAQ;AAC1B,MAAI,UAAU;AACd,MAAI,UAAU;AAEd,QAAM,MAAM,OAAO,QAAuB;AACxC,aAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC9C,YAAM,SAAS,IAAI,QAAQ,CAAC;AAC5B,YAAM,OAAO,MAAM,IAAI,OAAO,MAAM;AACpC,UAAI,CAAC,MAAM;AACT,cAAM,IAAI;AAAA,UACR,mCAAmC,OAAO,MAAM,eAAe,CAAC;AAAA,QAClE;AAAA,MACF;AACA,YAAM,cAAc,mBAAmB,IAAI;AAC3C,YAAM,OAAgC,EAAE,GAAG,OAAO,KAAK;AACvD,UAAI,YAAY,MAAM,UAAU,EAAG,MAAK,WAAW,MAAM;AACzD,UAAI,YAAY,MAAM,gBAAgB,EAAG,MAAK,iBAAiB,MAAM;AAErE,UAAI,OAAO,SAAS,OAAO,MAAM,QAAQ;AACvC,cAAM,QAAiC,CAAC;AACxC,mBAAW,SAAS,OAAO,MAAO,OAAM,KAAK,IAAI,KAAK,KAAK;AAC3D,YAAI,YAAY,MAAM,UAAU,EAAG,OAAM,WAAW,MAAM;AAC1D,YAAI,YAAY,MAAM,gBAAgB,EAAG,OAAM,iBAAiB,MAAM;AACtE,cAAM,WAAW,MAAM,IAAI,QAAQ,aAAoB,KAAY;AACnE,YAAI,UAAU;AACZ,qBAAW;AACX,kBAAQ,aAAa,EAAE,OAAO,GAAG,OAAO,QAAQ,OAAO,QAAQ,QAAQ,UAAU,CAAC;AAClF;AAAA,QACF;AAAA,MACF;AAEA,YAAM,SAAS,IAAI,OAAO,aAAoB,IAAW;AACzD,UAAI,QAAQ,MAAM;AAClB,YAAM,IAAI,MAAM;AAChB,iBAAW;AACX,cAAQ,aAAa,EAAE,OAAO,GAAG,OAAO,QAAQ,OAAO,QAAQ,QAAQ,UAAU,CAAC;AAAA,IACpF;AACA,QAAI,QAAQ,OAAQ,OAAM,IAAI,mBAAmB;AAAA,EACnD;AAEA,MAAI;AACF,UAAM,GAAG,cAAc,GAAG;AAAA,EAC5B,SAAS,KAAK;AACZ,QAAI,EAAE,eAAe,oBAAqB,OAAM;AAAA,EAClD;AAEA,SAAO,EAAE,OAAO,SAAS,QAAQ;AACnC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const SEED_DOCUMENT_FORMAT = "om-seed";
|
|
3
|
+
const SEED_DOCUMENT_VERSION = 1;
|
|
4
|
+
const ENCRYPTED_SEED_FORMAT = "om-encrypted-seed";
|
|
5
|
+
const ENCRYPTED_SEED_VERSION = 1;
|
|
6
|
+
const ENCRYPTED_SEED_ALGORITHM = "aes-256-gcm";
|
|
7
|
+
const seedRecordSchema = z.object({
|
|
8
|
+
entity: z.string().min(1),
|
|
9
|
+
match: z.array(z.string().min(1)).optional(),
|
|
10
|
+
data: z.record(z.string(), z.unknown())
|
|
11
|
+
});
|
|
12
|
+
const seedDocumentSchema = z.object({
|
|
13
|
+
format: z.literal(SEED_DOCUMENT_FORMAT),
|
|
14
|
+
version: z.literal(SEED_DOCUMENT_VERSION),
|
|
15
|
+
records: z.array(seedRecordSchema)
|
|
16
|
+
});
|
|
17
|
+
const encryptedSeedEnvelopeSchema = z.object({
|
|
18
|
+
format: z.literal(ENCRYPTED_SEED_FORMAT),
|
|
19
|
+
version: z.literal(ENCRYPTED_SEED_VERSION),
|
|
20
|
+
algorithm: z.literal(ENCRYPTED_SEED_ALGORITHM),
|
|
21
|
+
payload: z.string().min(1)
|
|
22
|
+
});
|
|
23
|
+
export {
|
|
24
|
+
ENCRYPTED_SEED_ALGORITHM,
|
|
25
|
+
ENCRYPTED_SEED_FORMAT,
|
|
26
|
+
ENCRYPTED_SEED_VERSION,
|
|
27
|
+
SEED_DOCUMENT_FORMAT,
|
|
28
|
+
SEED_DOCUMENT_VERSION,
|
|
29
|
+
encryptedSeedEnvelopeSchema,
|
|
30
|
+
seedDocumentSchema,
|
|
31
|
+
seedRecordSchema
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/seed/types.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\n\nexport const SEED_DOCUMENT_FORMAT = 'om-seed'\nexport const SEED_DOCUMENT_VERSION = 1\n\nexport const ENCRYPTED_SEED_FORMAT = 'om-encrypted-seed'\nexport const ENCRYPTED_SEED_VERSION = 1\nexport const ENCRYPTED_SEED_ALGORITHM = 'aes-256-gcm'\n\n/**\n * A single record to seed. `entity` is the platform entity id (`module:entity`,\n * e.g. `customers:customer_entity`). `data` keys are the entity's own property\n * names (camelCase, as declared on the MikroORM entity). `match`, when present,\n * lists property names used for an idempotent existence check before insert \u2014\n * these MUST be non-encrypted natural keys (id, slug, code, *_hash); encrypted\n * fields cannot be matched because their ciphertext is non-deterministic.\n */\nexport const seedRecordSchema = z.object({\n entity: z.string().min(1),\n match: z.array(z.string().min(1)).optional(),\n data: z.record(z.string(), z.unknown()),\n})\nexport type SeedRecord = z.infer<typeof seedRecordSchema>\n\n/**\n * The plaintext seed document. Records are applied in array order so authors can\n * satisfy foreign-key dependencies (create the parent before the child). The\n * document MUST NOT hard-code `tenantId`/`organizationId` \u2014 the loader injects\n * the target scope at load time so the same document seeds any tenant.\n */\nexport const seedDocumentSchema = z.object({\n format: z.literal(SEED_DOCUMENT_FORMAT),\n version: z.literal(SEED_DOCUMENT_VERSION),\n records: z.array(seedRecordSchema),\n})\nexport type SeedDocument = z.infer<typeof seedDocumentSchema>\n\n/**\n * The committed-to-repo, opaque envelope. `payload` is the encrypted seed\n * document in the shared AES-GCM `iv:ct:tag:v1` wire format.\n */\nexport const encryptedSeedEnvelopeSchema = z.object({\n format: z.literal(ENCRYPTED_SEED_FORMAT),\n version: z.literal(ENCRYPTED_SEED_VERSION),\n algorithm: z.literal(ENCRYPTED_SEED_ALGORITHM),\n payload: z.string().min(1),\n})\nexport type EncryptedSeedEnvelope = z.infer<typeof encryptedSeedEnvelopeSchema>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,uBAAuB;AAC7B,MAAM,wBAAwB;AAE9B,MAAM,wBAAwB;AAC9B,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAUjC,MAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS;AAAA,EAC3C,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AACxC,CAAC;AASM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,QAAQ,EAAE,QAAQ,oBAAoB;AAAA,EACtC,SAAS,EAAE,QAAQ,qBAAqB;AAAA,EACxC,SAAS,EAAE,MAAM,gBAAgB;AACnC,CAAC;AAOM,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAClD,QAAQ,EAAE,QAAQ,qBAAqB;AAAA,EACvC,SAAS,EAAE,QAAQ,sBAAsB;AAAA,EACzC,WAAW,EAAE,QAAQ,wBAAwB;AAAA,EAC7C,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAC3B,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7195.1.b5efd1d51a';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -42,16 +42,32 @@ function getEventRegistryState() {
|
|
|
42
42
|
return fallbackEventRegistryState;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const CRUD_AFTER_EVENT_SUFFIXES = [".created", ".updated", ".deleted"];
|
|
46
|
+
const DEFAULT_CRUD_PAYLOAD_SCHEMA = {
|
|
47
|
+
fields: [
|
|
48
|
+
{ path: "id", type: "text" },
|
|
49
|
+
{ path: "organizationId", type: "text", optional: true },
|
|
50
|
+
{ path: "tenantId", type: "text", optional: true },
|
|
51
|
+
{ path: "syncOrigin", type: "text", optional: true }
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
function applyDefaultCrudPayloadSchema(event) {
|
|
55
|
+
if (event.payloadSchema) return event;
|
|
56
|
+
if (event.category !== "crud") return event;
|
|
57
|
+
if (!CRUD_AFTER_EVENT_SUFFIXES.some((suffix) => event.id.endsWith(suffix))) return event;
|
|
58
|
+
return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA };
|
|
59
|
+
}
|
|
45
60
|
function addDeclaredEvent(event) {
|
|
61
|
+
const declared = applyDefaultCrudPayloadSchema(event);
|
|
46
62
|
const state = getEventRegistryState();
|
|
47
|
-
state.declaredEventIds.add(
|
|
48
|
-
const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id ===
|
|
63
|
+
state.declaredEventIds.add(declared.id);
|
|
64
|
+
const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id);
|
|
49
65
|
if (existingIndex < 0) {
|
|
50
|
-
state.declaredEvents.push(
|
|
66
|
+
state.declaredEvents.push(declared);
|
|
51
67
|
return;
|
|
52
68
|
}
|
|
53
|
-
if (state.declaredEvents[existingIndex]?.module ===
|
|
54
|
-
state.declaredEvents[existingIndex] =
|
|
69
|
+
if (state.declaredEvents[existingIndex]?.module === declared.module) {
|
|
70
|
+
state.declaredEvents[existingIndex] = declared;
|
|
55
71
|
}
|
|
56
72
|
}
|
|
57
73
|
function isEventDeclared(eventId) {
|
|
@@ -102,10 +118,12 @@ function getEventModuleConfigs() {
|
|
|
102
118
|
function createModuleEvents(options) {
|
|
103
119
|
const { moduleId, events, strict = false } = options;
|
|
104
120
|
const validEventIds = new Set(events.map((e) => e.id));
|
|
105
|
-
const fullEvents = events.map(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
const fullEvents = events.map(
|
|
122
|
+
(e) => applyDefaultCrudPayloadSchema({
|
|
123
|
+
...e,
|
|
124
|
+
module: moduleId
|
|
125
|
+
})
|
|
126
|
+
);
|
|
109
127
|
for (const event of fullEvents) {
|
|
110
128
|
addDeclaredEvent(event);
|
|
111
129
|
}
|
|
@@ -144,6 +162,7 @@ function createModuleEvents(options) {
|
|
|
144
162
|
};
|
|
145
163
|
}
|
|
146
164
|
export {
|
|
165
|
+
DEFAULT_CRUD_PAYLOAD_SCHEMA,
|
|
147
166
|
createModuleEvents,
|
|
148
167
|
getAllDeclaredEventIds,
|
|
149
168
|
getDeclaredEvents,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/events/factory.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const state = getEventRegistryState()\n state.declaredEventIds.add(event.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === event.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(event)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === event.module) {\n state.declaredEvents[existingIndex] = event\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added\n const fullEvents: EventDefinition[] = events.map(e => ({\n ...e,\n module: moduleId,\n }))\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAMA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["/**\n * Event Module Factory\n *\n * Provides factory functions for creating type-safe event configurations.\n */\n\nimport { createLogger } from '../../lib/logger'\nimport type {\n EventDefinition,\n EventModuleConfig,\n EventPayload,\n EventPayloadSchema,\n EmitOptions,\n CreateModuleEventsOptions,\n ModuleEventEmitter,\n} from './types'\n\nconst logger = createLogger('events').child({ component: 'factory' })\n\n// =============================================================================\n// Global Event Bus Reference\n// =============================================================================\n\n/**\n * Type for the global event bus interface\n */\ninterface GlobalEventBus {\n emit(event: string, payload: unknown, options?: EmitOptions): Promise<void>\n}\n\nconst GLOBAL_EVENT_BUS_KEY = '__openMercatoGlobalEventBus__'\n\n// Global event bus reference (set during bootstrap)\nlet globalEventBus: GlobalEventBus | null = null\n\n/**\n * Set the global event bus instance.\n * Called during app bootstrap to wire up event emission.\n */\nexport function setGlobalEventBus(bus: GlobalEventBus): void {\n globalEventBus = bus\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY] = bus\n } catch {\n // ignore global assignment failures\n }\n}\n\n/**\n * Get the global event bus instance.\n * Returns null if not yet bootstrapped.\n */\nexport function getGlobalEventBus(): GlobalEventBus | null {\n try {\n const sharedBus = (globalThis as Record<string, unknown>)[GLOBAL_EVENT_BUS_KEY]\n if (sharedBus && typeof sharedBus === 'object' && typeof (sharedBus as GlobalEventBus).emit === 'function') {\n return sharedBus as GlobalEventBus\n }\n } catch {\n // ignore global read failures\n }\n return globalEventBus\n}\n\n// =============================================================================\n// Event Registry for Validation\n// =============================================================================\n\ntype EventRegistryState = {\n declaredEventIds: Set<string>\n declaredEvents: EventDefinition[]\n registeredEventConfigs: EventModuleConfig[] | null\n}\n\nconst GLOBAL_EVENT_REGISTRY_KEY = '__openMercatoEventDefinitionRegistry__'\n\nconst fallbackEventRegistryState: EventRegistryState = {\n declaredEventIds: new Set<string>(),\n declaredEvents: [],\n registeredEventConfigs: null,\n}\n\nfunction isEventRegistryState(value: unknown): value is EventRegistryState {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<EventRegistryState>\n return candidate.declaredEventIds instanceof Set\n && Array.isArray(candidate.declaredEvents)\n && (candidate.registeredEventConfigs === null || Array.isArray(candidate.registeredEventConfigs))\n}\n\nfunction getEventRegistryState(): EventRegistryState {\n try {\n const globalScope = globalThis as Record<string, unknown>\n const existing = globalScope[GLOBAL_EVENT_REGISTRY_KEY]\n if (isEventRegistryState(existing)) return existing\n globalScope[GLOBAL_EVENT_REGISTRY_KEY] = fallbackEventRegistryState\n return fallbackEventRegistryState\n } catch {\n // Restricted runtimes may deny global access. Keep the previous\n // module-local behavior as a safe fallback.\n return fallbackEventRegistryState\n }\n}\n\nconst CRUD_AFTER_EVENT_SUFFIXES = ['.created', '.updated', '.deleted'] as const\n\n/**\n * Generated payload schema for platform-emitted CRUD after-events. Mirrors the\n * default payload built by the data engine's `emitOrmEntityEvent` when no\n * `buildPayload` override is configured: `{ id, organizationId, tenantId }`\n * plus `syncOrigin` when the write originated from a sync. organizationId and\n * tenantId keys are always present but may be null, so they are `optional`.\n */\nexport const DEFAULT_CRUD_PAYLOAD_SCHEMA: EventPayloadSchema = {\n fields: [\n { path: 'id', type: 'text' },\n { path: 'organizationId', type: 'text', optional: true },\n { path: 'tenantId', type: 'text', optional: true },\n { path: 'syncOrigin', type: 'text', optional: true },\n ],\n}\n\nfunction applyDefaultCrudPayloadSchema(event: EventDefinition): EventDefinition {\n if (event.payloadSchema) return event\n if (event.category !== 'crud') return event\n if (!CRUD_AFTER_EVENT_SUFFIXES.some(suffix => event.id.endsWith(suffix))) return event\n return { ...event, payloadSchema: DEFAULT_CRUD_PAYLOAD_SCHEMA }\n}\n\nfunction addDeclaredEvent(event: EventDefinition): void {\n const declared = applyDefaultCrudPayloadSchema(event)\n const state = getEventRegistryState()\n state.declaredEventIds.add(declared.id)\n const existingIndex = state.declaredEvents.findIndex((candidate) => candidate.id === declared.id)\n if (existingIndex < 0) {\n state.declaredEvents.push(declared)\n return\n }\n // Refresh a module's own definition in place during HMR without allowing a\n // duplicate declaration from another module to take over the event id.\n if (state.declaredEvents[existingIndex]?.module === declared.module) {\n state.declaredEvents[existingIndex] = declared\n }\n}\n\n/**\n * Check if an event ID has been declared by any module.\n * Used for runtime validation to ensure only declared events are emitted.\n */\nexport function isEventDeclared(eventId: string): boolean {\n return getEventRegistryState().declaredEventIds.has(eventId)\n}\n\n/**\n * Get all declared event IDs.\n * Useful for debugging and introspection.\n */\nexport function getAllDeclaredEventIds(): string[] {\n return Array.from(getEventRegistryState().declaredEventIds)\n}\n\n/**\n * Get all declared events with their full definitions.\n * Used by the API to return available events for workflow triggers.\n */\nexport function getDeclaredEvents(): EventDefinition[] {\n return [...getEventRegistryState().declaredEvents]\n}\n\n/**\n * Check if an event has clientBroadcast enabled.\n * Used by the SSE endpoint to filter events for the DOM Event Bridge.\n */\nexport function isBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true\n}\n\n/**\n * Check if an event should be published over the server-to-server event bridge.\n * Browser-broadcast events remain eligible for backward compatibility, while\n * crossProcessBroadcast supports private process coordination without SSE.\n */\nexport function isCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.clientBroadcast === true || event?.crossProcessBroadcast === true\n}\n\n/**\n * Check whether an event is reserved for private server-to-server\n * coordination. Workflow-authored EMIT_EVENT activities must not emit these\n * events because their payload and event id are tenant-managed input.\n */\nexport function isPrivateCrossProcessBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.crossProcessBroadcast === true && event?.clientBroadcast !== true\n}\n\n/**\n * Verify provenance for a private cross-process event. The module id is\n * stamped by a declared module emitter or another trusted server-side seam;\n * tenant-managed event payloads never participate in this decision.\n */\nexport function isPrivateCrossProcessEventEmitter(\n eventId: string,\n emitterModuleId: string | undefined,\n): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n if (event?.crossProcessBroadcast !== true) return true\n return typeof event.module === 'string'\n && event.module.length > 0\n && event.module === emitterModuleId\n}\n\n/**\n * Check if an event has portalBroadcast enabled.\n * Used by the portal SSE endpoint to filter events for the Portal Event Bridge.\n */\nexport function isPortalBroadcastEvent(eventId: string): boolean {\n const event = getEventRegistryState().declaredEvents.find(e => e.id === eventId)\n return event?.portalBroadcast === true\n}\n\n// =============================================================================\n// Bootstrap Registration (similar to searchModuleConfigs pattern)\n// =============================================================================\n\n/**\n * Register event module configurations globally.\n * Called during app bootstrap with configs from events.generated.ts.\n */\nexport function registerEventModuleConfigs(configs: EventModuleConfig[]): void {\n const state = getEventRegistryState()\n if (state.registeredEventConfigs !== null && process.env.NODE_ENV === 'development') {\n logger.debug('Event module configs re-registered (this may occur during HMR)')\n }\n state.registeredEventConfigs = configs\n for (const config of configs) {\n for (const event of config.events) {\n addDeclaredEvent(event)\n }\n }\n}\n\n/**\n * Get registered event module configurations.\n * Returns empty array if not registered.\n */\nexport function getEventModuleConfigs(): EventModuleConfig[] {\n return getEventRegistryState().registeredEventConfigs ?? []\n}\n\n// =============================================================================\n// Factory Function\n// =============================================================================\n\n/**\n * Creates a type-safe event configuration for a module.\n *\n * Usage in module events.ts:\n * ```typescript\n * import { createModuleEvents } from '@open-mercato/shared/modules/events'\n *\n * const events = [\n * { id: 'customers.people.created', label: 'Person Created', category: 'crud' },\n * { id: 'customers.people.updated', label: 'Person Updated', category: 'crud' },\n * ] as const\n *\n * export const eventsConfig = createModuleEvents({\n * moduleId: 'customers',\n * events,\n * })\n *\n * // Export the typed emit function for use in commands\n * export const emitCustomersEvent = eventsConfig.emit\n *\n * // Export event IDs as a type for external use\n * export type CustomersEventId = typeof events[number]['id']\n *\n * export default eventsConfig\n * ```\n *\n * TypeScript will enforce that only declared event IDs can be emitted:\n * ```typescript\n * // \u2705 This compiles - event is declared\n * emitCustomersEvent('customers.people.created', { id: '123', tenantId: 'abc' })\n *\n * // \u274C TypeScript error - event not declared\n * emitCustomersEvent('customers.people.exploded', { id: '123' })\n * ```\n */\nexport function createModuleEvents<\n const TEvents extends readonly { id: string }[],\n TEventIds extends TEvents[number]['id'] = TEvents[number]['id']\n>(options: CreateModuleEventsOptions<TEventIds>): EventModuleConfig<TEventIds> {\n const { moduleId, events, strict = false } = options\n\n // Build set of valid event IDs for runtime validation\n const validEventIds = new Set(events.map(e => e.id))\n\n // Build full event definitions with module added and the generated CRUD\n // payload-schema default applied, so config consumers and the global\n // registry see the same definitions.\n const fullEvents: EventDefinition[] = events.map(e =>\n applyDefaultCrudPayloadSchema({\n ...e,\n module: moduleId,\n }),\n )\n\n // Register all event IDs and definitions in the global registry.\n for (const event of fullEvents) {\n addDeclaredEvent(event)\n }\n\n /**\n * The emit function - validates events and delegates to the global event bus\n */\n const emit = async (\n eventId: TEventIds,\n payload: EventPayload,\n emitOptions?: EmitOptions\n ): Promise<void> => {\n // Runtime validation - event must be declared\n if (!validEventIds.has(eventId)) {\n const message =\n `[events] Module \"${moduleId}\" tried to emit undeclared event \"${eventId}\". ` +\n `Add it to the module's events.ts file first.`\n\n if (strict) {\n throw new Error(message)\n } else {\n logger.error('Module tried to emit undeclared event \u2014 add it to the module events.ts first', { moduleId, eventId })\n // In non-strict mode, still emit but with warning\n }\n }\n\n // Get event bus from global reference\n const eventBus = getGlobalEventBus()\n if (!eventBus) {\n logger.warn('Event bus not available, cannot emit event', { eventId })\n return\n }\n\n const eventDefinition = fullEvents.find((event) => event.id === eventId)\n const isClientBroadcast = eventDefinition?.clientBroadcast === true\n const trustedOptions = eventDefinition?.crossProcessBroadcast === true || isClientBroadcast\n ? {\n ...emitOptions,\n // Browser-broadcast module emitters historically accepted scope in\n // their typed payload. Preserve that contract at the trusted module\n // boundary while the event bus itself relies only on options.\n ...(isClientBroadcast && emitOptions?.tenantId === undefined\n ? { tenantId: payload.tenantId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationId === undefined\n ? { organizationId: payload.organizationId ?? null }\n : {}),\n ...(isClientBroadcast && emitOptions?.organizationIds === undefined && Array.isArray(payload.organizationIds)\n ? { organizationIds: payload.organizationIds.filter((value): value is string => typeof value === 'string') }\n : {}),\n emitterModuleId: moduleId,\n }\n : emitOptions\n await eventBus.emit(eventId, payload, trustedOptions)\n }\n\n return {\n moduleId,\n events: fullEvents,\n emit: emit as unknown as ModuleEventEmitter<TEventIds>,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7195.1.b5efd1d51a",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -113,10 +113,11 @@
|
|
|
113
113
|
"@mikro-orm/core": "^7.1.14",
|
|
114
114
|
"@mikro-orm/decorators": "^7.1.14",
|
|
115
115
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
116
|
-
"@open-mercato/cache": "0.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7195.1.b5efd1d51a",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|
|
120
|
+
"gpt-tokenizer": "^3.4.0",
|
|
120
121
|
"html-to-text": "^10.0.0",
|
|
121
122
|
"pino": "^10.3.1",
|
|
122
123
|
"rate-limiter-flexible": "^11.2.0",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeOpenCodeToolPart } from '../opencode-tool-parts'
|
|
2
|
+
|
|
3
|
+
describe('normalizeOpenCodeToolPart', () => {
|
|
4
|
+
it('opens a native tool part on a non-terminal state', () => {
|
|
5
|
+
const update = normalizeOpenCodeToolPart({
|
|
6
|
+
type: 'tool',
|
|
7
|
+
id: 'prt-1',
|
|
8
|
+
callID: 'call-1',
|
|
9
|
+
tool: 'load_skill',
|
|
10
|
+
state: { status: 'running', input: { skillId: 'x' } },
|
|
11
|
+
})
|
|
12
|
+
expect(update).toEqual({ phase: 'progress', callId: 'call-1', toolName: 'load_skill', input: { skillId: 'x' } })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('finishes a native tool part on completed, carrying the output', () => {
|
|
16
|
+
const update = normalizeOpenCodeToolPart({
|
|
17
|
+
type: 'tool',
|
|
18
|
+
callID: 'call-1',
|
|
19
|
+
tool: 'load_skill',
|
|
20
|
+
state: { status: 'completed', input: { skillId: 'x' }, output: { ok: true } },
|
|
21
|
+
})
|
|
22
|
+
expect(update).toEqual({
|
|
23
|
+
phase: 'finish',
|
|
24
|
+
callId: 'call-1',
|
|
25
|
+
toolName: 'load_skill',
|
|
26
|
+
input: { skillId: 'x' },
|
|
27
|
+
output: { ok: true },
|
|
28
|
+
status: 'ok',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('maps an errored native tool part to status error, preferring state.error', () => {
|
|
33
|
+
const update = normalizeOpenCodeToolPart({
|
|
34
|
+
type: 'tool',
|
|
35
|
+
callID: 'call-2',
|
|
36
|
+
tool: 'run_skill_script',
|
|
37
|
+
state: { status: 'error', error: 'boom' },
|
|
38
|
+
})
|
|
39
|
+
expect(update).toEqual({
|
|
40
|
+
phase: 'finish',
|
|
41
|
+
callId: 'call-2',
|
|
42
|
+
toolName: 'run_skill_script',
|
|
43
|
+
input: undefined,
|
|
44
|
+
output: 'boom',
|
|
45
|
+
status: 'error',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('falls back to part.id when callID is absent', () => {
|
|
50
|
+
const update = normalizeOpenCodeToolPart({
|
|
51
|
+
type: 'tool',
|
|
52
|
+
id: 'prt-9',
|
|
53
|
+
tool: 'search',
|
|
54
|
+
state: { status: 'running' },
|
|
55
|
+
})
|
|
56
|
+
expect(update).toMatchObject({ phase: 'progress', callId: 'prt-9', toolName: 'search' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('handles the legacy tool_use / tool_result shape', () => {
|
|
60
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool_use', id: 'tc-1', name: 'load_skill', input: { a: 1 } })).toEqual({
|
|
61
|
+
phase: 'progress',
|
|
62
|
+
callId: 'tc-1',
|
|
63
|
+
toolName: 'load_skill',
|
|
64
|
+
input: { a: 1 },
|
|
65
|
+
})
|
|
66
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool_result', tool_use_id: 'tc-1', content: { ok: true } })).toEqual({
|
|
67
|
+
phase: 'finish',
|
|
68
|
+
callId: 'tc-1',
|
|
69
|
+
output: { ok: true },
|
|
70
|
+
status: 'ok',
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('ignores non-tool and malformed parts', () => {
|
|
75
|
+
expect(normalizeOpenCodeToolPart({ type: 'text', text: 'hi' })).toBeNull()
|
|
76
|
+
expect(normalizeOpenCodeToolPart({ type: 'thinking' })).toBeNull()
|
|
77
|
+
expect(normalizeOpenCodeToolPart({ type: 'tool' })).toBeNull() // no callID/tool
|
|
78
|
+
expect(normalizeOpenCodeToolPart(null)).toBeNull()
|
|
79
|
+
expect(normalizeOpenCodeToolPart('nope')).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { countTokens, TOKEN_ENCODING } from '../token-count'
|
|
2
|
+
|
|
3
|
+
describe('countTokens', () => {
|
|
4
|
+
it('returns 0 for empty / nullish input', () => {
|
|
5
|
+
expect(countTokens('')).toBe(0)
|
|
6
|
+
expect(countTokens(null)).toBe(0)
|
|
7
|
+
expect(countTokens(undefined)).toBe(0)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('counts more tokens for longer text', () => {
|
|
11
|
+
const short = countTokens('hello')
|
|
12
|
+
const long = countTokens('hello world, this is a noticeably longer sentence.')
|
|
13
|
+
expect(short).toBeGreaterThan(0)
|
|
14
|
+
expect(long).toBeGreaterThan(short)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('exposes the encoding label', () => {
|
|
18
|
+
expect(TOKEN_ENCODING).toBe('o200k_base')
|
|
19
|
+
})
|
|
20
|
+
})
|