@ismaelsoilet/synapsejs 1.4.0 → 1.9.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 (53) hide show
  1. package/README.md +108 -10
  2. package/bin/synapse.ts +263 -218
  3. package/package.json +1 -1
  4. package/src/client/components.tsx +26 -9
  5. package/src/client/outlet.tsx +40 -0
  6. package/src/client/sub-slice.tsx +45 -0
  7. package/src/client/use-websocket.ts +146 -0
  8. package/src/client.ts +12 -0
  9. package/src/compiler/client-bundler.ts +17 -7
  10. package/src/compiler/context-bench.ts +7 -1
  11. package/src/compiler/contract.ts +31 -4
  12. package/src/compiler/db-schema-generator.ts +39 -8
  13. package/src/compiler/fields-parser.ts +28 -1
  14. package/src/compiler/impact-analyzer.ts +31 -1
  15. package/src/compiler/migration-runner.ts +144 -11
  16. package/src/compiler/optional-coverage.ts +137 -0
  17. package/src/compiler/oracle-runner.ts +8 -3
  18. package/src/compiler/scaffolder.ts +174 -3
  19. package/src/compiler/schema-dag.ts +6 -3
  20. package/src/compiler/slice-discovery.ts +8 -1
  21. package/src/compiler/slice-splitter.ts +67 -6
  22. package/src/compiler/slice-templates.ts +10 -0
  23. package/src/compiler/standalone-builder.ts +16 -2
  24. package/src/core/action-context.ts +101 -8
  25. package/src/core/cache.ts +139 -0
  26. package/src/core/config.ts +124 -10
  27. package/src/core/database-client.ts +68 -6
  28. package/src/core/index.ts +5 -0
  29. package/src/core/path-guard.ts +105 -0
  30. package/src/core/postgres-client.ts +6 -0
  31. package/src/core/postgres-queue.ts +45 -7
  32. package/src/core/query-builder.ts +75 -60
  33. package/src/core/session-context.ts +1 -1
  34. package/src/core/session-token.ts +115 -8
  35. package/src/core/sqlite-client.ts +26 -1
  36. package/src/core/storage.ts +61 -24
  37. package/src/core/topics.ts +84 -0
  38. package/src/core/webhook-signature.ts +151 -0
  39. package/src/core/websocket.ts +30 -0
  40. package/src/index.ts +10 -2
  41. package/src/mcp/server.ts +78 -173
  42. package/src/mcp/tools.ts +238 -0
  43. package/src/project-status.ts +330 -0
  44. package/src/runtime/discovery-rules.ts +20 -1
  45. package/src/runtime/event-hub.ts +6 -1
  46. package/src/runtime/network-guard.ts +182 -0
  47. package/src/runtime/queue-engine.ts +102 -21
  48. package/src/runtime/rate-limiter.ts +24 -0
  49. package/src/runtime/server.ts +1380 -251
  50. package/src/runtime/uploads.ts +199 -19
  51. package/src/version.ts +11 -0
  52. package/templates/starter/.jev.json +1 -1
  53. package/templates/starter/tsconfig.json +1 -0
package/bin/synapse.ts CHANGED
@@ -27,20 +27,91 @@ import { renderContractJson, renderContractMarkdown } from '../src/compiler/cont
27
27
  import { generateDatabaseSchemaCatalog } from '../src/compiler/db-schema-generator';
28
28
  import { analyzeImpact } from '../src/compiler/impact-analyzer';
29
29
  import { rollbackSliceMigrations, runSliceMigrations } from '../src/compiler/migration-runner';
30
+ import { analyzeOptionalItemCoverage, unionCoverage } from '../src/compiler/optional-coverage';
30
31
  import { runSliceOracles } from '../src/compiler/oracle-runner';
31
- import { scaffoldCrud, scaffoldSlice } from '../src/compiler/scaffolder';
32
+ import { scaffoldCrud, scaffoldShared, scaffoldSlice } from '../src/compiler/scaffolder';
32
33
  import { checkSchemaDrift } from '../src/compiler/schema-drift';
33
34
  import { findSliceFiles, resolveSlicesDir } from '../src/compiler/slice-discovery';
34
35
  import { artifactDirectory, splitSlice, verifySplit, writeSplitArtifacts } from '../src/compiler/slice-splitter';
35
36
  import { SLICE_TEMPLATES, type SliceTemplate } from '../src/compiler/slice-templates';
36
37
  import { buildStandalone } from '../src/compiler/standalone-builder';
38
+ import { isInsideBase } from '../src/core/path-guard';
37
39
  import { SynapseMcpServer } from '../src/mcp/server';
40
+ import { projectStatus } from '../src/project-status';
38
41
  import { SynapseServer } from '../src/runtime/server';
42
+ import { SYNAPSE_VERSION } from '../src/version';
39
43
 
40
44
  const command = process.argv[2] || 'check';
41
45
  const root = process.cwd();
42
46
 
47
+ /**
48
+ * The flags each subcommand accepts. An unrecognised flag is refused on every
49
+ * command, with the same machine-readable failure code — a machine consumer must
50
+ * not be told "PASS" for a flag the command silently ignored.
51
+ */
52
+ const KNOWN_FLAGS: Record<string, string[]> = {
53
+ dev: ['--watch'],
54
+ start: ['--acknowledge-insecure'],
55
+ check: [],
56
+ migrate: [],
57
+ rollback: ['--steps'],
58
+ mcp: [],
59
+ skeleton: [],
60
+ 'db-schema': [],
61
+ 'db-drift': [],
62
+ impact: [],
63
+ split: [],
64
+ 'new-slice': ['--template', '--fields'],
65
+ 'new-shared': [],
66
+ build: ['--standalone'],
67
+ contract: ['--markdown'],
68
+ test: ['--gate'],
69
+ new: [],
70
+ create: [],
71
+ init: [],
72
+ worker: [],
73
+ info: [],
74
+ coverage: []
75
+ };
76
+
77
+ /** Returns the first unrecognised flag, or null when every flag is declared. */
78
+ function unknownFlag(argv: string[]): string | null {
79
+ const known = KNOWN_FLAGS[command] ?? [];
80
+
81
+ for (const argument of argv.slice(3)) {
82
+ if (!argument.startsWith('--')) {
83
+ continue;
84
+ }
85
+
86
+ const name = argument.split('=')[0];
87
+
88
+ if (!known.includes(name)) {
89
+ return argument;
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
43
96
  async function main() {
97
+ const offendingFlag = unknownFlag(process.argv);
98
+
99
+ if (offendingFlag) {
100
+ process.stdout.write(
101
+ `${JSON.stringify(
102
+ {
103
+ status: 'FAIL',
104
+ code: 'UNKNOWN_FLAG',
105
+ operation: 'FLAG_VALIDATION',
106
+ message: `Flag desconhecida: ${offendingFlag}. Flags aceitas por '${command}': ${(KNOWN_FLAGS[command] ?? []).join(', ') || 'nenhuma'}.`
107
+ },
108
+ null,
109
+ 2
110
+ )}\n`
111
+ );
112
+ process.exit(1);
113
+ }
114
+
44
115
  switch (command) {
45
116
  case 'dev': {
46
117
  const watch = process.argv.includes('--watch');
@@ -62,12 +133,32 @@ async function main() {
62
133
 
63
134
  case 'start': {
64
135
  const port = parseInt(process.argv[3] || process.env.PORT || '3000', 10);
65
- const isProduction = process.env.NODE_ENV === 'production';
136
+ const { isProductionLikeMode } = await import('../src/runtime/server');
137
+ const acknowledged =
138
+ process.argv.includes('--acknowledge-insecure') ||
139
+ process.env.SYNAPSE_ACKNOWLEDGE_INSECURE === 'true' ||
140
+ process.env.SYNAPSE_DEV_HEADERS === 'true';
141
+
142
+ if (isProductionLikeMode() && !process.env.SYNAPSE_SESSION_SECRET && !acknowledged) {
143
+ process.stdout.write(
144
+ `${JSON.stringify(
145
+ {
146
+ status: 'FAIL',
147
+ operation: 'START',
148
+ code: 'MISSING_SESSION_SECRET',
149
+ message:
150
+ 'Modo de produção sem SYNAPSE_SESSION_SECRET: toda requisição resolveria para anônimo. Configure o segredo ou passe --acknowledge-insecure para assumir o risco explicitamente.'
151
+ },
152
+ null,
153
+ 2
154
+ )}\n`
155
+ );
156
+ process.exit(1);
157
+ }
66
158
 
67
- if (isProduction && !process.env.SYNAPSE_SESSION_SECRET) {
159
+ if (!process.env.SYNAPSE_SESSION_SECRET && acknowledged) {
68
160
  console.warn(
69
- '⚠️ [AVISO DE SEGURANÇA] NODE_ENV=production ativo sem SYNAPSE_SESSION_SECRET configurado!\n' +
70
- ' Sessões de usuário permanecerão anônimas por padrão.'
161
+ '⚠️ [AVISO DE SEGURANÇA] Iniciando sem SYNAPSE_SESSION_SECRET: identidade e papéis vindos de headers do chamador (x-user-id, x-user-roles) NÃO serão confiados; requisições permanecem anônimas.'
71
162
  );
72
163
  }
73
164
 
@@ -79,13 +170,24 @@ async function main() {
79
170
  const gracefulShutdown = async (signal: string) => {
80
171
  if (shuttingDown) return;
81
172
  shuttingDown = true;
82
- console.log(`\n🛑 [SynapseJS] Recebido sinal ${signal}, iniciando encerramento gracioso...`);
83
173
  try {
84
- await server.stop();
85
- console.log('✅ [SynapseJS] Servidor e conexões de persistência encerrados com sucesso.');
174
+ const summary = await server.stop();
175
+ process.stdout.write(
176
+ `${JSON.stringify({
177
+ status: 'PASS',
178
+ operation: 'GRACEFUL_SHUTDOWN',
179
+ signal,
180
+ drainedRequests: summary.drained,
181
+ abortedRequests: summary.aborted,
182
+ jobsRequeued: summary.jobsRequeued,
183
+ jobsFailed: summary.jobsFailed
184
+ })}\n`
185
+ );
86
186
  process.exit(0);
87
187
  } catch (err) {
88
- console.error('❌ [SynapseJS] Erro durante encerramento gracioso:', err);
188
+ process.stderr.write(
189
+ `${JSON.stringify({ status: 'ERROR', operation: 'GRACEFUL_SHUTDOWN', error: String(err) })}\n`
190
+ );
89
191
  process.exit(1);
90
192
  }
91
193
  };
@@ -257,7 +359,9 @@ async function main() {
257
359
 
258
360
  const report = analyzeImpact(target, root);
259
361
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
260
- process.exit(0);
362
+ // A target that does not exist is a failure; a valid target with no
363
+ // dependents is not.
364
+ process.exit(report.status === 'PASS' ? 0 : 1);
261
365
  break;
262
366
  }
263
367
 
@@ -341,7 +445,7 @@ async function main() {
341
445
  const name = process.argv[4];
342
446
 
343
447
  if (!domain || !name) {
344
- process.stderr.write(
448
+ process.stdout.write(
345
449
  `${JSON.stringify({
346
450
  status: 'ERROR',
347
451
  message: 'Parâmetros obrigatórios ausentes. Uso: synapse new-slice <domain> <name>'
@@ -409,7 +513,7 @@ async function main() {
409
513
  const created = scaffoldSlice(domain, name, root, template, fieldsArg);
410
514
 
411
515
  if (!created.ok) {
412
- process.stderr.write(
516
+ process.stdout.write(
413
517
  `${JSON.stringify(
414
518
  {
415
519
  status: 'ERROR',
@@ -442,6 +546,53 @@ async function main() {
442
546
  break;
443
547
  }
444
548
 
549
+ case 'new-shared': {
550
+ const name = process.argv[3];
551
+ if (!name) {
552
+ process.stdout.write(
553
+ `${JSON.stringify({
554
+ status: 'ERROR',
555
+ message: 'Nome do módulo compartilhado ausente. Uso: synapse new-shared <nome>'
556
+ })}\n`
557
+ );
558
+ process.exit(1);
559
+ }
560
+
561
+ const root = process.env.SYNAPSE_ROOT || process.cwd();
562
+ const created = scaffoldShared(name, root);
563
+
564
+ if (!created.ok) {
565
+ process.stdout.write(
566
+ `${JSON.stringify(
567
+ {
568
+ status: 'ERROR',
569
+ operation: 'SCAFFOLD_SHARED',
570
+ code: created.error.code,
571
+ message: created.error.message
572
+ },
573
+ null,
574
+ 2
575
+ )}\n`
576
+ );
577
+ process.exit(1);
578
+ }
579
+
580
+ process.stdout.write(
581
+ `${JSON.stringify(
582
+ {
583
+ status: 'PASS',
584
+ operation: 'SCAFFOLD_SHARED',
585
+ name,
586
+ createdPath: path.relative(root, created.value)
587
+ },
588
+ null,
589
+ 2
590
+ )}\n`
591
+ );
592
+ process.exit(0);
593
+ break;
594
+ }
595
+
445
596
  case 'build': {
446
597
  const isStandalone = process.argv.includes('--standalone');
447
598
 
@@ -525,6 +676,52 @@ async function main() {
525
676
  break;
526
677
  }
527
678
 
679
+ case 'coverage': {
680
+ // Which optional contract items the committed slices exercise — across every
681
+ // shipped application, so the report covers the union.
682
+ const targets = [
683
+ root,
684
+ ...(fs.existsSync(path.join(root, 'examples'))
685
+ ? fs
686
+ .readdirSync(path.join(root, 'examples'), { withFileTypes: true })
687
+ .filter((entry) => entry.isDirectory())
688
+ .map((entry) => path.join(root, 'examples', entry.name))
689
+ : [])
690
+ ];
691
+
692
+ const reports = targets.map((target) => analyzeOptionalItemCoverage(target));
693
+ const resolvable = reports.filter((report) => report.status !== 'FAIL' || report.totalSlices > 0);
694
+
695
+ if (resolvable.length === 0) {
696
+ process.stdout.write(
697
+ `${JSON.stringify(reports[0] ?? { status: 'FAIL', operation: 'OPTIONAL_ITEM_COVERAGE' }, null, 2)}\n`
698
+ );
699
+ process.exit(1);
700
+ }
701
+
702
+ const union = unionCoverage(resolvable);
703
+
704
+ process.stdout.write(
705
+ `${JSON.stringify(
706
+ {
707
+ status: union.unexercised.length === 0 ? 'PASS' : 'FAIL',
708
+ operation: 'OPTIONAL_ITEM_COVERAGE',
709
+ applications: resolvable.map((report) => ({
710
+ appDir: report.appDir,
711
+ totalSlices: report.totalSlices,
712
+ items: report.items
713
+ })),
714
+ covered: union.covered,
715
+ unexercised: union.unexercised
716
+ },
717
+ null,
718
+ 2
719
+ )}\n`
720
+ );
721
+ process.exit(union.unexercised.length === 0 ? 0 : 1);
722
+ break;
723
+ }
724
+
528
725
  case 'contract': {
529
726
  // O framework se descreve numa chamada: e o que o agente le em vez de adivinhar.
530
727
  const asMarkdown = process.argv.includes('--markdown');
@@ -550,7 +747,7 @@ async function main() {
550
747
  case 'init': {
551
748
  const rawTarget = process.argv[3];
552
749
  if (!rawTarget) {
553
- process.stderr.write(
750
+ process.stdout.write(
554
751
  `${JSON.stringify({
555
752
  status: 'ERROR',
556
753
  message: 'Nome do projeto obrigatório. Uso: synapse new <project-name>'
@@ -560,15 +757,34 @@ async function main() {
560
757
  }
561
758
 
562
759
  const targetDir = path.resolve(root, rawTarget);
760
+
761
+ if (!isInsideBase(root, targetDir)) {
762
+ process.stdout.write(
763
+ `${JSON.stringify(
764
+ {
765
+ status: 'ERROR',
766
+ operation: 'CREATE_PROJECT',
767
+ code: 'INVALID_PATH_SEGMENT',
768
+ message: `O caminho '${rawTarget}' resolve para fora do diretório atual (${root}). Um projeto novo é criado dentro dele; use um nome simples ou um subdiretório.`
769
+ },
770
+ null,
771
+ 2
772
+ )}\n`
773
+ );
774
+ process.exit(1);
775
+ }
776
+
563
777
  const cleanProjectName = path
564
778
  .basename(targetDir)
565
779
  .toLowerCase()
566
780
  .replace(/[^a-z0-9-_]/g, '-');
567
781
 
568
782
  if (fs.existsSync(targetDir)) {
569
- process.stderr.write(
783
+ process.stdout.write(
570
784
  `${JSON.stringify({
571
785
  status: 'ERROR',
786
+ operation: 'CREATE_PROJECT',
787
+ code: 'TARGET_EXISTS',
572
788
  message: `O diretório '${rawTarget}' já existe em ${targetDir}.`
573
789
  })}\n`
574
790
  );
@@ -586,9 +802,11 @@ async function main() {
586
802
  const templateDir = candidates.find((dir) => fs.existsSync(dir));
587
803
 
588
804
  if (!templateDir) {
589
- process.stderr.write(
805
+ process.stdout.write(
590
806
  `${JSON.stringify({
591
807
  status: 'ERROR',
808
+ operation: 'CREATE_PROJECT',
809
+ code: 'TEMPLATE_NOT_FOUND',
592
810
  message: 'Template oficial starter não encontrado nos diretórios candidatos.'
593
811
  })}\n`
594
812
  );
@@ -613,9 +831,11 @@ async function main() {
613
831
  let content = fs.readFileSync(s, 'utf-8');
614
832
  if (entry.name === 'package.json') {
615
833
  content = content.replace('"starter-app"', `"${cleanProjectName}"`);
834
+ // The scaffold depends on the version this CLI ships, never on a frozen
835
+ // older pin: a new project must receive every fix since then.
616
836
  content = content.replace(
617
837
  /"(@ismaelsoilet\/)?synapsejs":\s*"[^"]+"/g,
618
- '"@ismaelsoilet/synapsejs": "^1.1.1"'
838
+ `"@ismaelsoilet/synapsejs": "^${SYNAPSE_VERSION}"`
619
839
  );
620
840
  }
621
841
  fs.writeFileSync(d, content, 'utf-8');
@@ -665,6 +885,7 @@ async function main() {
665
885
  typeof exportVal === 'object' &&
666
886
  'name' in (exportVal as Record<string, unknown>)
667
887
  ) {
888
+ // biome-ignore lint/suspicious/noExplicitAny: slice export boundary, validated by the shape check above
668
889
  queue.registerJob(exportVal as any);
669
890
  }
670
891
  }
@@ -677,9 +898,21 @@ async function main() {
677
898
  console.log('⚡ [SynapseJS Worker] Fila de background jobs ativa e processando...');
678
899
  let running = true;
679
900
  const shutdown = () => {
901
+ if (!running) return;
680
902
  running = false;
681
- console.log('\n🛑 [SynapseJS Worker] Encerrando worker...');
903
+ // Stop claiming first, then return whatever was running to the queue with its
904
+ // attempt counter already advanced by the interrupted claim.
905
+ queue.stopClaiming();
906
+ const interrupted = queue.interruptRunningJobs();
682
907
  queue.close();
908
+ process.stdout.write(
909
+ `${JSON.stringify({
910
+ status: 'PASS',
911
+ operation: 'WORKER_SHUTDOWN',
912
+ jobsRequeued: interrupted.requeued,
913
+ jobsFailed: interrupted.failed
914
+ })}\n`
915
+ );
683
916
  process.exit(0);
684
917
  };
685
918
  process.on('SIGINT', shutdown);
@@ -703,215 +936,27 @@ async function main() {
703
936
  }
704
937
 
705
938
  case 'info': {
939
+ // One machine-readable source: version, adoption, tool inventory and feature
940
+ // status all come from the same module the docs gate reads.
941
+ process.stdout.write(`${JSON.stringify(projectStatus(), null, 2)}\n`);
942
+ process.exit(0);
943
+ break;
944
+ }
945
+
946
+ default: {
706
947
  process.stdout.write(
707
948
  `${JSON.stringify(
708
949
  {
709
- framework: 'SynapseJS',
710
- version: '1.1.0',
711
- runtime: 'Bun + Bun.serve',
712
- database: 'Embedded SQLite (WAL) and PostgreSQL, both verified',
713
- protocols: ['REST/HTTP', 'Isomorphic RPC', 'Model Context Protocol (MCP)'],
714
- features: [
715
- {
716
- feature: 'Vertical slices (N = 1) with Locality of Behavior',
717
- status: 'stable',
718
- evidence: 'bun test packages/synapse/test/locality.test.ts'
719
- },
720
- {
721
- feature: 'Result<T, E> control flow (no public throwing helper)',
722
- status: 'stable',
723
- evidence: 'bun test packages/synapse/test/machine-types.test.ts'
724
- },
725
- {
726
- feature: 'TypeBox JIT input contracts',
727
- status: 'stable',
728
- evidence: 'bun test packages/synapse/test/slice-contract.test.ts'
729
- },
730
- {
731
- feature: 'Declarative sliceSchema migrations, applied once per statement',
732
- status: 'stable',
733
- evidence: 'bun test packages/synapse/test/migration-runner.test.ts'
734
- },
735
- {
736
- feature: 'Domain errors map to HTTP status (401/403/404/409/422/500)',
737
- status: 'stable',
738
- evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
739
- },
740
- {
741
- feature: 'React hydration from a client bundle the splitter produces',
742
- status: 'stable',
743
- evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
744
- },
745
- {
746
- feature: 'Signed sessions (SYNAPSE_SESSION_SECRET) and static files from public/',
747
- status: 'stable',
748
- evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
749
- },
750
- {
751
- feature: 'Login with Bun.password, signed token and cookie session (storeSession/clearSession)',
752
- status: 'stable',
753
- evidence:
754
- 'bun run --cwd apps/crm synapse test (login oracle: the server accepts the issued token) + bun test packages/synapse/test/session-cookie.test.ts'
755
- },
756
- {
757
- feature: 'Cross-slice atomicity through src/shared modules (db.transaction, rollback proven on SQLite)',
758
- status: 'stable',
759
- evidence: 'bun test packages/synapse/test/shared-modules.test.ts'
760
- },
761
- {
762
- feature: 'Slice boundary: a slice never imports another slice (SLICE_IMPORTS_SLICE)',
763
- status: 'stable',
764
- evidence: 'bun test packages/synapse/test/slice-splitter.test.ts'
765
- },
766
- {
767
- feature: 'Browser RPC transport that never throws (RPC_MALFORMED/RPC_UNREACHABLE as values)',
768
- status: 'stable',
769
- evidence: 'bun test packages/synapse/test/rpc-client.test.ts'
770
- },
771
- {
772
- feature: 'Pre-built client bundles with a manifest (synapse build)',
773
- status: 'stable',
774
- evidence: 'bun test packages/synapse/test/client-bundler.test.ts'
775
- },
776
- {
777
- feature: 'Uploads with session, size limit and traversal guard (/_synapse/files)',
778
- status: 'stable',
779
- evidence: 'bun test packages/synapse/test/uploads.test.ts'
780
- },
781
- {
782
- feature: 'Embedded SQLite engine (WAL, prepared-statement cache)',
783
- status: 'stable',
784
- evidence: 'bun test packages/synapse/test/sqlite-client.test.ts'
785
- },
786
- {
787
- feature: 'Zero-wiring routing, SSR shell and RPC dispatcher',
788
- status: 'stable',
789
- evidence: 'bun --cwd examples/enterprise-crm test:e2e'
790
- },
791
- {
792
- feature: 'Slice discovery resolution (never reports PASS with zero slices)',
793
- status: 'stable',
794
- evidence: 'bun test packages/synapse/test/slice-discovery.test.ts'
795
- },
796
- {
797
- feature: 'MCP stdio server (11 tools)',
798
- status: 'stable',
799
- evidence: 'bun test packages/synapse/test/mcp-server.test.ts'
800
- },
801
- {
802
- feature: 'AST skeletonizer to .codebase/repo-map.d.ts',
803
- status: 'stable',
804
- evidence: 'bun --cwd examples/enterprise-crm skeleton'
805
- },
806
- {
807
- feature: 'Centralized Database Schema Catalog (.codebase/db-schema.d.ts)',
808
- status: 'stable',
809
- evidence: 'bun test packages/synapse/test/db-schema-generator.test.ts'
810
- },
811
- {
812
- feature: 'Unified Object Storage (LocalStorage + AWS S3/R2 with SigV4)',
813
- status: 'stable',
814
- evidence: 'bun test packages/synapse/test/storage.test.ts'
815
- },
816
- {
817
- feature: 'Distributed PostgreSQL Queue (SKIP LOCKED, Full Jitter, DLQ)',
818
- status: 'stable',
819
- evidence: 'bun test packages/synapse/test/postgres-queue.test.ts'
820
- },
821
- {
822
- feature: 'Isomorphic slice splitter (shared/server/client modules)',
823
- status: 'stable',
824
- evidence: 'bun test packages/synapse/test/slice-splitter.test.ts'
825
- },
826
- {
827
- feature: 'Slice invariants executed by bun:test (per-invariant reporting)',
828
- status: 'stable',
829
- evidence: 'bun test packages/synapse/test/oracle-runner.test.ts'
830
- },
831
- {
832
- feature: 'Incremental diagnostics cache across processes',
833
- status: 'roadmap',
834
- evidence: 'removido na 0.4.0: medido mais lento que o check completo (2.5s vs 1.9s)'
835
- },
836
- {
837
- feature: 'PostgreSQL parity (migrations, DDL, action round-trip)',
838
- status: 'stable',
839
- evidence: 'bun run test:postgres (CI roda contra um serviço postgres:16)'
840
- },
841
- {
842
- feature: 'Context surface benchmark (files/tokens an agent must read)',
843
- status: 'stable',
844
- evidence: 'bun run bench'
845
- },
846
- {
847
- feature: 'Isolated SQLite Queue & Background Jobs (defineJob, exponential backoff)',
848
- status: 'stable',
849
- evidence: 'bun test packages/synapse/test/jobs-queue.test.ts'
850
- },
851
- {
852
- feature: 'Bidirectional DDL migrations with transactional rollback (synapse rollback)',
853
- status: 'stable',
854
- evidence: 'bun test packages/synapse/test/onda2.test.ts'
855
- },
856
- {
857
- feature: 'Vendor code-splitting and dynamic metadata (sliceMeta)',
858
- status: 'stable',
859
- evidence: 'bun test packages/synapse/test/onda1.test.ts'
860
- },
861
- {
862
- feature: 'Hierarchical domain layouts (_layout.tsx)',
863
- status: 'stable',
864
- evidence: 'bun test packages/synapse/test/onda1.test.ts'
865
- },
866
- {
867
- feature: 'Sliding-window rate limiting & streaming upload guard',
868
- status: 'stable',
869
- evidence: 'bun test packages/synapse/test/onda2.test.ts'
870
- },
871
- {
872
- feature: 'Distributed PostgreSQL Event Hub & Isomorphic i18n',
873
- status: 'stable',
874
- evidence: 'bun test packages/synapse/test/onda3.test.ts'
875
- },
876
- {
877
- feature: 'Token revocation, TOTP 2FA, image optimization & plugin hooks',
878
- status: 'stable',
879
- evidence: 'bun test packages/synapse/test/onda4.test.ts'
880
- }
881
- ],
882
- commands: [
883
- 'new',
884
- 'dev',
885
- 'start',
886
- 'check',
887
- 'migrate',
888
- 'rollback',
889
- 'db-drift',
890
- 'impact',
891
- 'mcp',
892
- 'skeleton',
893
- 'db-schema',
894
- 'split',
895
- 'build',
896
- 'test',
897
- 'worker',
898
- 'new-slice',
899
- 'contract',
900
- 'info'
901
- ]
950
+ status: 'FAIL',
951
+ code: 'UNKNOWN_COMMAND',
952
+ operation: 'COMMAND_VALIDATION',
953
+ message: `Comando desconhecido: ${command}.`,
954
+ commands: Object.keys(KNOWN_FLAGS)
902
955
  },
903
956
  null,
904
957
  2
905
958
  )}\n`
906
959
  );
907
- process.exit(0);
908
- break;
909
- }
910
-
911
- default: {
912
- process.stderr.write(
913
- `Unknown command: ${command}\nAvailable: new, dev, check, migrate, rollback, mcp, skeleton, db-schema, split, build, test, worker, new-slice, contract, info\n`
914
- );
915
960
  process.exit(1);
916
961
  }
917
962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ismaelsoilet/synapsejs",
3
- "version": "1.4.0",
3
+ "version": "1.9.0",
4
4
  "description": "Bun-only fullstack framework where a feature is a single contiguous file",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -336,32 +336,49 @@ export interface DataFormProps {
336
336
  className?: string;
337
337
  }
338
338
 
339
- export function setNestedProperty(obj: Record<string, any>, path: string, value: unknown): void {
339
+ const FORBIDDEN_PROPERTIES = new Set(['__proto__', 'constructor', 'prototype']);
340
+
341
+ export function setNestedProperty(obj: Record<string, unknown>, path: string, value: unknown): void {
340
342
  const parts = path.split('.');
343
+ if (parts.some((p) => FORBIDDEN_PROPERTIES.has(p.trim()))) {
344
+ return;
345
+ }
341
346
  let cur = obj;
342
347
  for (let i = 0; i < parts.length - 1; i++) {
343
348
  const part = parts[i];
349
+ if (FORBIDDEN_PROPERTIES.has(part)) return;
344
350
  if (!cur[part] || typeof cur[part] !== 'object') {
345
- cur[part] = {};
351
+ cur[part] = Object.create(null);
346
352
  }
347
- cur = cur[part];
353
+ cur = cur[part] as Record<string, unknown>;
354
+ }
355
+ const lastKey = parts[parts.length - 1];
356
+ if (!FORBIDDEN_PROPERTIES.has(lastKey)) {
357
+ cur[lastKey] = value;
348
358
  }
349
- cur[parts[parts.length - 1]] = value;
350
359
  }
351
360
 
352
- export function getNestedProperty(obj: Record<string, any>, path: string): unknown {
361
+ export function getNestedProperty(obj: Record<string, unknown>, path: string): unknown {
353
362
  const parts = path.split('.');
354
- let cur: any = obj;
363
+ if (parts.some((p) => FORBIDDEN_PROPERTIES.has(p.trim()))) {
364
+ return undefined;
365
+ }
366
+ let cur: unknown = obj;
355
367
  for (const part of parts) {
356
- if (cur === null || cur === undefined) return undefined;
357
- cur = cur[part];
368
+ if (cur === null || cur === undefined || typeof cur !== 'object' || FORBIDDEN_PROPERTIES.has(part)) {
369
+ return undefined;
370
+ }
371
+ cur = (cur as Record<string, unknown>)[part];
358
372
  }
359
373
  return cur;
360
374
  }
361
375
 
362
376
  export function expandNestedObject(flat: Record<string, unknown>): Record<string, unknown> {
363
- const nested: Record<string, any> = {};
377
+ const nested: Record<string, unknown> = {};
364
378
  for (const [key, value] of Object.entries(flat)) {
379
+ if (FORBIDDEN_PROPERTIES.has(key.trim())) {
380
+ continue;
381
+ }
365
382
  if (key.includes('.')) {
366
383
  setNestedProperty(nested, key, value);
367
384
  } else {