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/.env.example +8 -0
- package/README.md +107 -1
- package/RELEASE_NOTES.md +12 -9
- package/SERVICE_CONTRACT.md +12 -0
- package/START_HERE.md +9 -0
- package/XOONYA_NATIVE_PROOF_FORMAT.md +52 -9
- package/index.js +12 -3
- package/lib/adapter_certification_contract.js +90 -0
- package/lib/authority_binding_lifecycle_contract.js +182 -0
- package/lib/crypto_agility_matrix.js +125 -0
- package/lib/machine_identity_registry_contract.js +199 -0
- package/lib/offline_verifiability_recovery_contract.js +155 -0
- package/lib/operational_receipt_contract.js +165 -0
- package/lib/public_surface_policy_contract.js +111 -0
- package/lib/signed_audit_provenance_contract.js +93 -0
- package/lib/trust_portability_boundary_contract.js +66 -0
- package/package.json +5 -2
- package/proof_sdk.js +327 -31
- package/reference/python/__pycache__/xnya_reference.cpython-314.pyc +0 -0
- package/scripts/verify_control_plane_contracts.js +34 -0
- package/scripts/verify_control_plane_routes.js +70 -0
- package/scripts/verify_forgery_resistance.js +281 -0
- package/scripts/verify_service.js +98 -0
- package/server.js +242 -16
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
|
-
|
|
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
|
-
|
|
680
|
-
|
|
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 (
|
|
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(
|
|
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',
|
|
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.
|
|
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
|
|
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
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
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
|
|