xoonya 3.0.1 → 3.1.1

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/server.js CHANGED
@@ -85,6 +85,21 @@ const {
85
85
  getTrustPortabilityProfile,
86
86
  summarizeTrustPortabilityPack
87
87
  } = require('./lib/trust_portability_pack');
88
+ const {
89
+ buildAdapterCertificationContractPack,
90
+ summarizeAdapterCertificationContractPack,
91
+ getAdapterCertificationFrontDetail
92
+ } = require('./lib/adapter_certification_contract');
93
+ const {
94
+ buildTrustPortabilityBoundaryContractPack,
95
+ summarizeTrustPortabilityBoundaryContractPack,
96
+ getTrustPortabilityAxisDetail
97
+ } = require('./lib/trust_portability_boundary_contract');
98
+ const {
99
+ buildSignedAuditProvenanceContractPack,
100
+ summarizeSignedAuditProvenanceContractPack,
101
+ getSignedAuditProvenanceSurfaceDetail
102
+ } = require('./lib/signed_audit_provenance_contract');
88
103
  const {
89
104
  buildProofTruthModelPack,
90
105
  summarizeProofTruthModelPack,
@@ -672,40 +687,109 @@ const {
672
687
  summarizePhase3DecisionResolutionPack
673
688
  } = require('./lib/phase_3_decision_resolution_pack');
674
689
 
690
+ const MAX_JSON_BODY_BYTES = 1024 * 1024;
691
+ const REQUEST_TIMEOUT_MS = 30000;
692
+ const HEADERS_TIMEOUT_MS = 15000;
693
+ const PRIVATE_KEY_FIELD_PATTERN = /private_key/i;
694
+ const PRIVATE_KEY_MATERIAL_ENV = 'XOONYA_ACCEPT_PRIVATE_KEY_MATERIAL';
695
+
675
696
  function readJsonBody(req) {
676
697
  return new Promise((resolve, reject) => {
677
- let body = '';
698
+ const declaredLength = Number(req.headers['content-length']);
699
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_JSON_BODY_BYTES) {
700
+ reject(new Error('request body too large'));
701
+ req.resume();
702
+ return;
703
+ }
704
+ const chunks = [];
705
+ let receivedBytes = 0;
706
+ let settled = false;
678
707
  req.on('data', (chunk) => {
679
- body += chunk;
680
- if (body.length > 1024 * 1024) {
708
+ if (settled) return;
709
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
710
+ receivedBytes += buffer.length;
711
+ if (receivedBytes > MAX_JSON_BODY_BYTES) {
712
+ settled = true;
681
713
  reject(new Error('request body too large'));
682
714
  req.destroy();
715
+ return;
683
716
  }
717
+ chunks.push(buffer);
684
718
  });
685
719
  req.on('end', () => {
686
- if (!body) {
720
+ if (settled) return;
721
+ settled = true;
722
+ if (receivedBytes === 0) {
687
723
  resolve({});
688
724
  return;
689
725
  }
690
726
  try {
691
- resolve(JSON.parse(body));
727
+ resolve(JSON.parse(Buffer.concat(chunks, receivedBytes).toString('utf8')));
692
728
  } catch (error) {
693
729
  reject(new Error('body must be valid JSON'));
694
730
  }
695
731
  });
696
- req.on('error', reject);
732
+ req.on('error', (error) => {
733
+ if (settled) return;
734
+ settled = true;
735
+ reject(error);
736
+ });
697
737
  });
698
738
  }
699
739
 
700
740
  function sendJson(res, statusCode, payload) {
701
741
  const json = JSON.stringify(payload);
702
- res.writeHead(statusCode, {
742
+ const method = res.req && res.req.method ? res.req.method : 'GET';
743
+ const headers = {
703
744
  'content-type': 'application/json; charset=utf-8',
704
745
  'content-length': Buffer.byteLength(json),
705
- });
746
+ 'x-content-type-options': 'nosniff',
747
+ };
748
+ if (method !== 'GET' && method !== 'HEAD') {
749
+ headers['cache-control'] = 'no-store';
750
+ }
751
+ res.writeHead(statusCode, headers);
706
752
  res.end(json);
707
753
  }
708
754
 
755
+ function acceptsPrivateKeyMaterial() {
756
+ const raw = String(process.env[PRIVATE_KEY_MATERIAL_ENV] || '').trim().toLowerCase();
757
+ if (raw === 'false' || raw === '0' || raw === 'no') return false;
758
+ if (raw === 'true' || raw === '1' || raw === 'yes') return true;
759
+ return !process.env.RENDER;
760
+ }
761
+
762
+ function findPrivateKeyMaterialPath(value, trail = '', depth = 0) {
763
+ if (!value || typeof value !== 'object' || depth > 16) return null;
764
+ const entries = Array.isArray(value)
765
+ ? value.map((item, index) => [String(index), item])
766
+ : Object.entries(value);
767
+ for (const [key, child] of entries) {
768
+ const path = trail ? `${trail}.${key}` : key;
769
+ if (!Array.isArray(value) && PRIVATE_KEY_FIELD_PATTERN.test(key)) return path;
770
+ const nested = findPrivateKeyMaterialPath(child, path, depth + 1);
771
+ if (nested) return nested;
772
+ }
773
+ return null;
774
+ }
775
+
776
+ function rejectPrivateKeyMaterial(res, body) {
777
+ if (acceptsPrivateKeyMaterial()) return false;
778
+ const fieldPath = findPrivateKeyMaterialPath(body);
779
+ if (!fieldPath) return false;
780
+ sendJson(res, 400, {
781
+ ok: false,
782
+ valid: false,
783
+ reason: 'private_key_material_not_accepted',
784
+ field: fieldPath,
785
+ detail: 'This deployment does not accept private key material over HTTP. Sign locally with the xoonya SDK (emitReceipt / buildProofBundle) or the xoonya CLI and submit the signed receipt or bundle instead.',
786
+ sdk_ref: 'npm:xoonya#emitReceipt',
787
+ cli_ref: 'xoonya --help',
788
+ policy_env: PRIVATE_KEY_MATERIAL_ENV,
789
+ });
790
+ return true;
791
+ }
792
+
709
793
  function payloadBufferFromBody(body) {
710
794
  if (typeof body.payload_bytes_ascii === 'string') {
711
795
  return Buffer.from(body.payload_bytes_ascii, 'ascii');
@@ -726,6 +810,20 @@ function canonicalEventFromBody(body) {
726
810
  async function routeRequest(req, res) {
727
811
  const url = new URL(req.url, 'http://127.0.0.1');
728
812
 
813
+ if (req.method === 'OPTIONS') {
814
+ res.writeHead(204, {
815
+ allow: 'GET, HEAD, POST, OPTIONS',
816
+ 'x-content-type-options': 'nosniff',
817
+ 'cache-control': 'no-store',
818
+ });
819
+ res.end();
820
+ return;
821
+ }
822
+ if (req.method === 'HEAD') {
823
+ // Route HEAD exactly like GET; Node already suppresses the response body for HEAD requests.
824
+ req.method = 'GET';
825
+ }
826
+
729
827
  if (req.method === 'GET' && url.pathname === '/health') {
730
828
  sendJson(res, 200, {
731
829
  status: 'ok',
@@ -811,6 +909,93 @@ async function routeRequest(req, res) {
811
909
  return;
812
910
  }
813
911
 
912
+ if (req.method === 'GET' && url.pathname === '/v1/adapter-certification') {
913
+ sendJson(res, 200, buildAdapterCertificationContractPack());
914
+ return;
915
+ }
916
+
917
+ if (req.method === 'GET' && url.pathname === '/v1/adapter-certification/summary') {
918
+ sendJson(res, 200, summarizeAdapterCertificationContractPack());
919
+ return;
920
+ }
921
+
922
+ if (req.method === 'GET' && url.pathname.startsWith('/v1/adapter-certification/')) {
923
+ const frontId = decodeURIComponent(url.pathname.slice('/v1/adapter-certification/'.length));
924
+ if (frontId && frontId !== 'summary') {
925
+ const front = getAdapterCertificationFrontDetail(frontId);
926
+ if (!front) {
927
+ sendJson(res, 404, { ok: false, error: 'adapter_certification_front_not_found' });
928
+ return;
929
+ }
930
+ sendJson(res, 200, {
931
+ ok: true,
932
+ product: 'xoonya',
933
+ category: 'naxytra-adapter-certification-detail',
934
+ front
935
+ });
936
+ return;
937
+ }
938
+ }
939
+
940
+ if (req.method === 'GET' && url.pathname === '/v1/trust-portability-boundary') {
941
+ sendJson(res, 200, buildTrustPortabilityBoundaryContractPack());
942
+ return;
943
+ }
944
+
945
+ if (req.method === 'GET' && url.pathname === '/v1/trust-portability-boundary/summary') {
946
+ sendJson(res, 200, summarizeTrustPortabilityBoundaryContractPack());
947
+ return;
948
+ }
949
+
950
+ if (req.method === 'GET' && url.pathname.startsWith('/v1/trust-portability-boundary/')) {
951
+ const axisId = decodeURIComponent(url.pathname.slice('/v1/trust-portability-boundary/'.length));
952
+ if (axisId && axisId !== 'summary') {
953
+ const axis = getTrustPortabilityAxisDetail(axisId);
954
+ if (!axis) {
955
+ sendJson(res, 404, { ok: false, error: 'trust_portability_axis_not_found' });
956
+ return;
957
+ }
958
+ sendJson(res, 200, {
959
+ ok: true,
960
+ product: 'xoonya',
961
+ category: 'naxytra-trust-portability-boundary-detail',
962
+ axis: {
963
+ ...axis,
964
+ axis_id: axis.axis_id || axis.profile_id || axis.portability_axis || null
965
+ }
966
+ });
967
+ return;
968
+ }
969
+ }
970
+
971
+ if (req.method === 'GET' && url.pathname === '/v1/signed-audit-provenance') {
972
+ sendJson(res, 200, buildSignedAuditProvenanceContractPack());
973
+ return;
974
+ }
975
+
976
+ if (req.method === 'GET' && url.pathname === '/v1/signed-audit-provenance/summary') {
977
+ sendJson(res, 200, summarizeSignedAuditProvenanceContractPack());
978
+ return;
979
+ }
980
+
981
+ if (req.method === 'GET' && url.pathname.startsWith('/v1/signed-audit-provenance/')) {
982
+ const surfaceId = decodeURIComponent(url.pathname.slice('/v1/signed-audit-provenance/'.length));
983
+ if (surfaceId && surfaceId !== 'summary') {
984
+ const surface = getSignedAuditProvenanceSurfaceDetail(surfaceId);
985
+ if (!surface) {
986
+ sendJson(res, 404, { ok: false, error: 'signed_audit_provenance_surface_not_found' });
987
+ return;
988
+ }
989
+ sendJson(res, 200, {
990
+ ok: true,
991
+ product: 'xoonya',
992
+ category: 'naxytra-signed-audit-provenance-detail',
993
+ surface
994
+ });
995
+ return;
996
+ }
997
+ }
998
+
814
999
  if (req.method === 'GET' && url.pathname === '/v1/proof-truth') {
815
1000
  sendJson(res, 200, buildProofTruthModelPack());
816
1001
  return;
@@ -2688,6 +2873,7 @@ async function routeRequest(req, res) {
2688
2873
  if (req.method === 'POST' && url.pathname === '/v1/proof-center/default-loops/run') {
2689
2874
  try {
2690
2875
  const body = await readJsonBody(req);
2876
+ if (rejectPrivateKeyMaterial(res, body)) return;
2691
2877
  const loopRef = String(body.loop_ref || '').trim();
2692
2878
  const result = proofSdk.runDefaultProofLoop(loopRef, body);
2693
2879
  sendJson(res, 200, result);
@@ -2733,6 +2919,7 @@ async function routeRequest(req, res) {
2733
2919
  if (req.method === 'POST' && url.pathname === '/v1/proof-center/transaction-handoff/run') {
2734
2920
  try {
2735
2921
  const body = await readJsonBody(req);
2922
+ if (rejectPrivateKeyMaterial(res, body)) return;
2736
2923
  const result = proofSdk.runTransactionProofHandoff(body);
2737
2924
  sendJson(res, 200, result);
2738
2925
  } catch (error) {
@@ -2744,6 +2931,7 @@ async function routeRequest(req, res) {
2744
2931
  if (req.method === 'POST' && url.pathname === '/v1/proof-center/interaction-spine-handoff/run') {
2745
2932
  try {
2746
2933
  const body = await readJsonBody(req);
2934
+ if (rejectPrivateKeyMaterial(res, body)) return;
2747
2935
  const result = proofSdk.runInteractionSpineProofHandoff(body);
2748
2936
  sendJson(res, 200, result);
2749
2937
  } catch (error) {
@@ -2755,6 +2943,7 @@ async function routeRequest(req, res) {
2755
2943
  if (req.method === 'POST' && url.pathname === '/v1/proof-center/result-package-handoff/run') {
2756
2944
  try {
2757
2945
  const body = await readJsonBody(req);
2946
+ if (rejectPrivateKeyMaterial(res, body)) return;
2758
2947
  const result = proofSdk.runInteractionResultPackageHandoff(body);
2759
2948
  sendJson(res, 200, result);
2760
2949
  } catch (error) {
@@ -3874,6 +4063,7 @@ async function routeRequest(req, res) {
3874
4063
  if (req.method === 'POST' && url.pathname === '/v1/receipts/emit') {
3875
4064
  try {
3876
4065
  const body = await readJsonBody(req);
4066
+ if (rejectPrivateKeyMaterial(res, body)) return;
3877
4067
  const event = canonicalEventFromBody(body);
3878
4068
  const receipt = proofSdk.emitReceipt(event, body.options || {});
3879
4069
  sendJson(res, 200, receipt);
@@ -3909,6 +4099,7 @@ async function routeRequest(req, res) {
3909
4099
  if (req.method === 'POST' && url.pathname === '/v1/proof-bundles/build') {
3910
4100
  try {
3911
4101
  const body = await readJsonBody(req);
4102
+ if (rejectPrivateKeyMaterial(res, body)) return;
3912
4103
  const bundle = proofSdk.buildProofBundle(body);
3913
4104
  sendJson(res, 200, bundle);
3914
4105
  } catch (error) {
@@ -3920,7 +4111,10 @@ async function routeRequest(req, res) {
3920
4111
  if (req.method === 'POST' && url.pathname === '/v1/proof-bundles/verify') {
3921
4112
  try {
3922
4113
  const body = await readJsonBody(req);
3923
- const result = proofSdk.verifyProofBundle(body.bundle);
4114
+ const verifyOptions = body.options && typeof body.options === 'object' && !Array.isArray(body.options)
4115
+ ? body.options
4116
+ : {};
4117
+ const result = proofSdk.verifyProofBundle(body.bundle, verifyOptions);
3924
4118
  sendJson(res, 200, result);
3925
4119
  } catch (error) {
3926
4120
  sendJson(res, 400, { valid: false, reason: error.message });
@@ -3972,23 +4166,55 @@ async function routeRequest(req, res) {
3972
4166
  });
3973
4167
  }
3974
4168
 
4169
+ function handleRouteError(req, res, error) {
4170
+ if (res.headersSent) {
4171
+ res.destroy();
4172
+ return;
4173
+ }
4174
+ if (error instanceof URIError) {
4175
+ sendJson(res, 400, { ok: false, reason: 'invalid_path_encoding' });
4176
+ return;
4177
+ }
4178
+ if (error && error.code === 'ERR_INVALID_URL') {
4179
+ sendJson(res, 400, { ok: false, reason: 'invalid_path' });
4180
+ return;
4181
+ }
4182
+ console.error(`[xoonya] internal_error ${req.method} ${req.url}\n${error && error.stack ? error.stack : error}`);
4183
+ sendJson(res, 500, { ok: false, reason: 'internal_error' });
4184
+ }
4185
+
3975
4186
  function createServer() {
3976
- return http.createServer((req, res) => {
3977
- routeRequest(req, res).catch((error) => {
3978
- sendJson(res, 500, {
3979
- status: 'error',
3980
- reason: error.message,
3981
- });
3982
- });
4187
+ const server = http.createServer((req, res) => {
4188
+ let routed;
4189
+ try {
4190
+ routed = routeRequest(req, res);
4191
+ } catch (error) {
4192
+ handleRouteError(req, res, error);
4193
+ return;
4194
+ }
4195
+ routed.catch((error) => handleRouteError(req, res, error));
3983
4196
  });
4197
+ server.requestTimeout = REQUEST_TIMEOUT_MS;
4198
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
4199
+ return server;
3984
4200
  }
3985
4201
 
3986
4202
  if (require.main === module) {
3987
4203
  const port = Number(process.env.PORT || process.env.XOONYA_PORT || 4310);
3988
4204
  const host = process.env.XOONYA_HOST || (process.env.RENDER ? '0.0.0.0' : '127.0.0.1');
3989
4205
  const server = createServer();
4206
+ process.on('unhandledRejection', (reason) => {
4207
+ console.error(`[xoonya] unhandledRejection\n${reason && reason.stack ? reason.stack : reason}`);
4208
+ });
4209
+ process.on('uncaughtException', (error) => {
4210
+ console.error(`[xoonya] uncaughtException, shutting down\n${error && error.stack ? error.stack : error}`);
4211
+ const forceExit = setTimeout(() => process.exit(1), 5000);
4212
+ forceExit.unref();
4213
+ server.close(() => process.exit(1));
4214
+ });
3990
4215
  server.listen(port, host, () => {
3991
4216
  console.log(`xoonya listening on http://${host}:${port}`);
4217
+ console.log(`xoonya private key material over HTTP: ${acceptsPrivateKeyMaterial() ? 'accepted' : 'rejected'} (${PRIVATE_KEY_MATERIAL_ENV})`);
3992
4218
  });
3993
4219
  }
3994
4220